一个合法检测的Validator

合法检测是在有表单或数据提交时常见的需求,随手撸了一个。由于考虑的业务场景比较复杂,代码也有点臃肿。拓展时,只需要修改上面的提示信息常量和新的test方法即可。也支持自己传入判断函数和message,针对比较复杂的校验情况。

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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
const TYPE_VALIDATE_MESSAGE = {
isNumber: '不是一个数字',
isObject: '不是一个对象',
isArray: '不是一个数组',
lt: '数值过大',
gt: '数值过小',
st: '长度过短',
ht: '长度过长'
notEmpty: '需要非空',
len: '长度不合规范',
in: '不在可选范围内',
reg: '无法匹配指定模式'
}

// 供validate使用
util.test = {
isObject: _ => Object(_) === _,
isNumber: _ => typeof _ == 'number',
isArray: _ => Array.isArray(_),
notEmpty: _ => _.length > 0,
lt: (num, limit) => num < limit,
gt: (num, limit) => num > limit,
st: (_, limit) => _.length < limit,
ht: (_, limit) => _.length > limit,
in: (_, range) => ~range.indexOf(_),
len: (_, length) => _.length === length,
reg: (_, reg) => reg.test(_)
}

/**
* 数据校验
* @author: shenlvmeng
* @params value {any} 校验数值 输入单个数值使用verify校验 输入对象使用verifyMap校验
* @method verify 校验单个数值
* @method verifyMap 校验对象
* @return {Object} 包含success和message的对象
*/
util.validate = (value) => {
// 方便传递到外界修改
let message = { content: '' };
function verify(val, validators, key) {
if (!Array.isArray(validators)) validators = [validators];
return validators.every(v => {
let isRight;
if (util.test.isObject(v)) {
isRight = util.test[Object.keys(v)[0]](val, Object.values(v)[0])
!isRight && (message.content = `${key || val}${TYPE_VALIDATE_MESSAGE[Object.keys(v)[0]]}`);
} else {
isRight = util.test[v](val);
!isRight && (message.content = `${key || val}${TYPE_VALIDATE_MESSAGE[v]}`);
}
return isRight;
});
}
return {
/**
* 单个数值使用
* @params validators { String|Array|Object } 校验函数 必须是util中已定义的函数
* @params String 类型时为函数名; Object类型时为函数名和对应入参; Array时为以上的列表
*/
verify(validators) {
return {
success: verify(value, validators),
message: message.content,
}
},
/**
* 校验整个对象时使用
* @params config { Object } 校验函数 针对Object中每个key的校验
* @params 每个key的value服从verify的格式,当key为function类型时交给调用方自己处理
*/
verifyMap(config) {
if (!util.test.isObject(value)) throw new Error('Invalid value type. It should be an Object.');
return {
success: Object.keys(config).every(key => (
typeof config[key] === 'function' ? config[key](value[key], message)
: verify(value[key], config[key], key))
),
message: message.content,
}
}
}
}

实际应用中发现,verifyMap方法用的比较多。使用样例如下;

1
2
3
4
5
6
7
8
9
10
examine() {
return util.validate(formData).verifyMap({
userName: ['notEmpty', { st: 6 }, { ht: 15 }],
password: ['notEmpty', { st: 6 }, { ht: 15 }],
age: ['isNumber', { lt: 18 }],
gender: {in: ['male', 'female']},
email: {'reg': /email pattern/},
isAccepted: (value, message) => value ? value : (message.content = '请接收用户协议', false)
});
},

Linux清理大文件

  • df -h查看存储情况
  • du -sh * | grep G查找当前目录下大于1G的大文件,挨个确认删除即可

当前页面窗口活动状态检查

利用visibilityChangeAPI,配合visibilityState检查窗口状态。

1
2
3
4
5
6
7
8
9
10
11
// subscribe to visibility change events
document.addEventListener('visibilitychange', function () {
// fires when user switches tabs, apps, goes to homescreen, etc.
if (document.visibilityState === 'hidden') {
document.title = 'Baby, Come Back!'
}
// fires when app transitions from prerender, user returns to the app / tab.
if (document.visibilityState === 'visible') {
document.title = defaultTitle;
}
});

IntersectionObserver

监测页面元素和视口的交错关系的DOM API,目前浏览器支持度不高。可以用来方便地实现懒加载和页面无限滚动。

参考

图种

  • Windows,copy /b image.jpg+zip.rar output.jpg,更改输出图片后缀为rar得到压缩包
  • Linux,cat image.jpg zip.rar > output.jpg,同上

draggable with Vue

业务中有拖拽需求,在试用多个轮子后,最终选择了vue-slicksort

  • Vue Draggable
  • Vue Dragula
  • Vue slicksort
GitHub名 流行度 依赖/原理 功能完善度 文档完善度 使用舒适度 备注
vuedraggable star数3859,很流行 依赖sortable.js,后者依赖HTML5的draggable API 功能丰富强大,有很多别人踩过的坑 文档比较丰富 通过组件包裹形式使用 侵入性小 优 有兼容性问题,Chrome66.0.3359.181 Mac OSX 10.13.1下无法通过拖动换位
vue-dragula star数100+,上次更新1年前 依赖dragula.js 功能一般 不完善,几乎都是dragula的API 通过Vue.use的形式 + directives的方式引入,侵入式一般 使用效果不好,实现时没有拖动和移动的动效
vue-slicksort star数100+,更新较活跃 0依赖,不基于HTML5 drag API,支持触摸屏,兼容性好 功能一般,支持平面拖动 较完善,有样例、API解释和FAQ 略糟糕,需要使用mixin的形式,通常意味着额外的组件定义 展示效果优秀,能满足需求

综上,考虑到只有vue-slicksort能满足需求,故选择之。

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
0%