git case sensitive

git本身是大小写敏感的。但在大小写不敏感的系统里,需要用hack方法记录仅修改文件名大小写的改动。

1
2
3
git mv file.txt temp.txt
git mv temp.txt File.txt
git commit -m "Renamed file.txt to File.txt"

webpack的一些经验

DefinePlugin

允许创建一个在编译时可以配置的全局常量。在构建区分环境的包时很有用。

1
2
3
4
5
6
7
new webpack.DefinePlugin({
PRODUCTION: JSON.stringify(true),
VERSION: JSON.stringify("5fa3b9"),
BROWSER_SUPPORTS_HTML5: true,
TWO: "1+1",
"typeof window": JSON.stringify("object")
})

注意:这个插件直接执行文本替换。因此:

  • 如果这个值是一个字符串,它会被当作一个代码片段来使用。
  • 如果这个值不是字符串,它会被转化为字符串(包括函数)。
  • 如果这个值是一个对象,它所有的 key 会被同样的方式定义。
  • 如果在一个 key 前面加了 typeof,它会被定义为 typeof 调用

resolve alias

创建import或require的别名,来确保模块引入变得更简单。例如,一些位于 src/ 文件夹下的常用模块:

1
2
3
4
5
alias: {
@: path.resolve(__dirname, 'src/'),
Utilities: path.resolve(__dirname, 'src/utilities/'),
Templates: path.resolve(__dirname, 'src/templates/')
}

z-index可能的坑

使用前提:z-index只能在position属性值为relative或absolute或fixed的元素上有效。

z-index值只决定同一父元素中的同级子元素的堆叠顺序。父元素的z-index值(如果有)为子元素定义了堆叠顺序(css版堆叠“拼爹”)。向上追溯找不到含有z-index值的父元素的情况下,则可以视为自由的z-index元素,它可以与父元素的同级兄弟定位元素或其他自由的定位元素来比较z-index的值,决定其堆叠顺序。同级元素的z-index值如果相同,则堆叠顺序由元素在文档中的先后位置决定,后出现的会在上面。

git submodule

参考:GIT 子模块

最新一个项目里要复用已有的一个git库的代码,具体来说就是要将之前在WebView的内容复刻到PC版完成(这个需求貌似应该还挺常见的)。为了保证代码复用性,选择了git submodule的方法。这也是我此前从没用过的一个命令。

简单来说,是一个 GIT 仓库下面某个文件夹的来源可以跟本库的来源不同,这个文件夹连接着别的库,由别的库负责按本控制和管理。是不是和npm包管理的形式比较像。子模块可以手动添加,也可以在克隆一个主库的时候就直接实体化。具体来说,有四种情况:

  • 克隆库的时候要初始化子模块 => 加上--recursive参数 git clone --recursive git@github.com:shenlvmeng/trace-maker.git
  • 初始化已有库的子模块 => git submodule update --init --recursive
  • 从子模块的源更新该子模块 => git submodule update --recursive --remote
  • 添加一个新的子模块 => git submodule add <git address> <folder address>

已有有git submodule的库内,.gitmodules是下面的样子:

1
2
3
[submodule "wheel"]
path = wheel
url = git@github.com:shenlvmeng/wheel.git

npm install

npm install后跟的绝不仅仅只是包名,还可以通过ssh、http的形式引入npm包,唯一的要求是有package.json

1
2
3
4
5
6
7
8
9
10
11
12
npm install (with no args, in package dir)
npm install [<@scope>/]<name>
npm install [<@scope>/]<name>@<tag>
npm install [<@scope>/]<name>@<version>
npm install [<@scope>/]<name>@<version range>
npm install <git-host>:<git-user>/<repo-name>
npm install <git repo url>
npm install <tarball file>
npm install <tarball url>
npm install <folder>

alias: npm i

一个package可以是下面的形式:

  1. 包含package.json的工程文件夹
  2. gzip过的“1”的压缩包
  3. 指向“2”的url
  4. 发布在npm-registry的<name>@<registry>字符串
  5. 发布在npm-registry的<name>@<tag>字符串
  6. 发布在npm-registry的<name>字符串(最新版本)
  7. 一个指向“1”的合法git地址

cleave.js

一个自动格式化输入框的工具,有npm包、script标签等几种引用形式,还有react的使用方式。

地址:Format your <input/> content when you are typing

object-fit & object-position

这两个CSS属性分别用于指定替换元素在其盒模型内的覆盖大小和对齐方式。使用效果很类似background-sizebackground-position。替换元素即内容不受CSS视觉格式化控制的元素,如image、iframe、video、textarea等。

这使得本来自己决定模型大小的元素可以受CSS控制决定位置排布和大小。在需要自适应元素大小的场景下很好用,比如用户头像展示等。

唯一的小小缺憾可能是IE11还不支持这两个属性,以及Edge只支持对<img>使用。

移动端触摸默认行为优化

  • user-select: none 禁止用户选择
  • -webkit-touch-callout: none 防止长按contextmenu弹出。类似的还有contextmenu事件里的e.preventDefault()
  • -webkit-tap-highlight-color: transparent 删除可点击元素默认的黑影

上传进度条

利用xhr事件的onprogress事件。

1
2
3
4
5
6
7
8
9
10
11
xhr.onprogress = function (e) {
if (e.lengthComputable) {
console.log(e.loaded+ " / " + e.total)
}
}
xhr.onloadstart = function (e) {
console.log("start")
}
xhr.onloadend = function (e) {
console.log("end")
}

不显示滚动条

基于Webkit的浏览器,可以使用CSS的方式隐藏滚动条。

1
2
3
4
5
&::-webkit-scrollbar {
width: 0;
height: 0;
background: transparent;
}

keyup无法prevent default

keyup fires after the default action.

keydown and keypress are where you can prevent the default.
If those aren’t stopped, then the default happens and keyup is fired.

来源:jquery - javascript prevent default for keyup - StackOverflow

mixin in react

版本16之前,可以用mixin特性。16之后使用高阶组件HOC + ES6 class语法实现。参考

user-select在Edge浏览器下的适配问题

设置user-selectnone在Edge浏览器下会导致input无法输入内容。可以用下面的写法,避免对input标签应用该属性。

1
2
3
*:not(input) {
user-select: none;
}

参考:html - Can’t type in input field using Microsoft Edge and Safari - StackOverflow

浏览器跨tab通信

最近业务遇到了一个需求:同一浏览器上多tab用户信息同步的问题,所有这个域名下的需要强制一样的用户信息,避免困惑。

跨tab通信主流方案有两种:

  • localStorage,利用window的storage事件,传递信息
  • BroadcastChannel,新的API,通过postMessageonMessage完成双向通讯
1
2
3
var bc = new BroadcastChannel('test_channel');
bc.postMessage('This is a test message.'); /* send */
bc.onmessage = function (ev) { console.log(ev); } /* receive */

后者还未得到广泛支持,需要前者进行polyfill。

aos

Animation on scroll。michalsnik/aos at master · Animate on scroll library.元素滚动至中的CSS动画,适合实现官网、落地页等效果。

extract-text-webpack-plugin

抽出CSS/Less/Sass等样式作为单独文件,用于那些需要提前加载样式的页面。详细用法见github

坑:

  1. 不支持webpack4.x,报内部错误(2018/07/30) => 使用@next下载最新版
  2. 报错Module build failed: ReferenceError: window is not defined => style-loader在extract-text-webpack-plugin中只做fallback项使用,见issue#503

常见调试技巧

  • 代码中插入debugger可以在该位置触发断点调试
  • console.dir可以打印对象结构,大多数情况和console.log表现一致,在document等DOM元素上表现不同

react组件复用设计思路

  • 当设计的组件为自闭型时,通过传入数据(不要传入功能)props的方式定制组件
  • 当设计的组件在有些场景下需要外部传入功能才能完整时,使用继承的方式实现
  • 在可以拆分出原子组件,且有此必要的时候,使用原子组件拼装业务组件
  • HOC优于mixin

前端科技树探索之路

前端的涵盖范畴是和客户直接交互的部分。

以下几个因素促进了前端在这些年差异化成一个需要专业人才的领域

  • Web API不断丰富和更新,允许JS做更多的事情,同时也需要不停学习跟进
  • 语言规范赋予JS更多的特性和可能,允许专业的人做更专业的事
  • Node等轮子的出现拓展了JS的应用场景,方向更加细化
  • 用户对界面要求越来越高,需要专门的人处理

从而使前端渐渐分化出来成为一个面向复杂场景、承诺服务质量、进入工程领域的职业。

更细化的说,面向复杂场景包括:

  • 浏览器应用、桌面应用、移动端应用、后端应用多宿主
  • 复杂的网络环境
  • 差异化巨大的浏览器和浏览器版本(所幸比以前好了很多)
  • 用户群体的不同
  • ……

承诺服务质量包括:

  • 更快地渲染页面
  • 更美观的页面效果
  • 更流畅的用户交互体验
  • 更高的代码稳定性(对应着lint和debug能力)
  • 差异化环境的表现一致性
  • ……

进入工程领域包括:

  • 更舒适的开发体验(设计模式与诸多轮子)
  • 更高的开发效率(如工作流的设计)
  • 更顺滑的团队间协作(如mock)
  • 版本控制
  • ……

上面是作为一个技术的要求,在公司应用范畴,还需要考虑下面这些:

  • 产品设计

  • 团队建设

  • 人才培养

  • 项目管理

  • 立身之本

    • HTML
    • DOM
    • JavaScript基本语法
    • CSS
  • 关联技术

    • Web API
    • ajax
    • JSON
    • 正则
    • SVG/Canvas/WebGL
    • PWA
  • 深入了解

    • ES6 ES7
    • TypeScript
    • CSS3
    • SASS Less
  • 现有轮子

    • NodeJS
    • Electron
    • React

实现流程图和类流程图的工具主要需要解决数据 -> 图形交互两方面问题。在实现图形元素时也有canvas,SVG,canvas with DOM,SVG with DOM,DOM with canvas一些实现方式。

canvas和SVG的实现方式区别比较明显:

  • 大规模元素、频繁重绘上,canvas完胜
  • 强调光影效果上,canvas小胜
  • 强调导出图片上,canvas小胜
  • 强调元素可交互上,SVG完胜
  • 强调画图元素可缩放上,SVG完胜

使用SVG实现时,元素规模大以及频繁重绘时会出现卡顿现象,在大规模元素场景下交互也会有卡顿。使用canvas实现时,保证流程图元素的可交互性将成为头疼的难题,开发者需要自己模拟浏览器的一部分行为。

下面是一些流程图实现基础的对比。

d3

d3着眼在数据可视化,重点在使用不同layout组织数据,完成可视化。

d3最初是天然支持SVG的,这点从类jQuery的API也能看出来。d3和canvas的结合上,绘制需要额外的data binding操作,周期性地将虚拟的DOM节点映射到canvas上,重绘得到下一帧画面。要实现canvas可交互的话也需要一些hack的手段。基于d3实现流程图并不划算。

zrender

zrender是一个canvas画图的基础库。它并不负责数据的组织和渲染,需要自己完成这一部分工作。但是zrender提供了让canvas可交互的重要功能。

zrender下,mixin了Eventful特性的元素上可以监听交互事件。Eventful只是为元素提供了类似EventEmitter的功能。真正实现元素可交互的handler。

handler内会拦截发生在canvas内的click/mousedown/mouseup/mousewheel/dblclick/contextmenu事件,交予prototype内对应的处理方法处理,handler内有下面几个关键方法:

  • mousemove,监听canvas内mousemove事件,调用findHover得到当前位置对应的元素,根据情况调用dispatchToElement方法,分发mouseoutmouseovermousemove给刚才得到的元素实例
  • dispatchToElement,分发事件到对应实例,将事件对象封装,trigger实例的对应事件handler,并通过el.parent向上冒泡
  • findHover,指定x, y坐标寻找该坐标位置的元素。从storage中拿到所有的displayable的list。挨个调用isHover判断displayable和[x, y]坐标的关系
  • isHover函数,根据displayable的rectHover属性,即是否使用包围盒检测鼠标进入。调用displayable的rectContaincontain检测是否在其中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function isHover(displayable, x, y) {
if (displayable[displayable.rectHover ? 'rectContain' : 'contain'](x, y)) {
var el = displayable;
var isSilent;
while (el) {
// If clipped by ancestor.
// FIXME: If clipPath has neither stroke nor fill,
// el.clipPath.contain(x, y) will always return false.
if (el.clipPath && !el.clipPath.contain(x, y)) {
return false;
}
if (el.silent) {
isSilent = true;
}
el = el.parent;
}
return isSilent ? SILENT : true;
}

return false;
}

先简单看下storage,因为zrender里绘制的元素之间没有逻辑关联,因此需要有一个全局存储storage去统一管理加入的Group或Shape。storage的getDisplayList方法返回所有图形的绘制队列。

1
2
3
4
5
6
7
getDisplayList: function (update, includeIgnore) {
includeIgnore = includeIgnore || false;
if (update) {
this.updateDisplayList(includeIgnore);
}
return this._displayList;
},

注:方法中提到的updateDisplayList用于更新图形的绘制队列,在每次绘制前调用,它会深度优先遍历整个树,更新所有的变换后,根据优先级排序得到新的绘制队列。

在displayable的基类中,contain方法只是单纯调用了rectContain(子类都有区别于rectContain的自己的实现)。在rectContain中,获取到坐标相对于图形的坐标(transformCoordToLocal)和图形的包围盒(getBoundingRect)。这里先说简单的RectContain

1
2
3
4
5
rectContain: function (x, y) {
var coord = this.transformCoordToLocal(x, y);
var rect = this.getBoundingRect();
return rect.contain(coord[0], coord[1]);
}

其中getBoundingRect是各自类自己实现的。除了个别情况,如Text,形状都基于Path类。在Path的getBoundingRect中可以看到,path的绘制又额外包装了一层PathProxygetBoundingRect也是使用的PathProxy的方法。在实现上,PathProxy把绘制路径的操作命令拆分成了命令数组。通过记录每一段子路径上x、y的最大最小值,再将所有这些极值比较得到最后的最值。在PathProxy返回结果后,根据描边粗细得到最终结果。

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
getBoundingRect: function () {
// _rect变量做缓存用,计算完成后只在重绘时置空,避免重复计算
var rect = this._rect;
var style = this.style;
var needsUpdateRect = !rect;
if (needsUpdateRect) {
var path = this.path;
if (!path) {
// Create path on demand.
path = this.path = new PathProxy();
}
if (this.__dirtyPath) {
path.beginPath();
this.buildPath(path, this.shape, false);
}
rect = path.getBoundingRect();
}
this._rect = rect;

if (style.hasStroke()) {
// ...

// Return rect with stroke
return rectWithStroke;
}

return rect;
},

Displayable继承自ElementElement通过mixin得到来自Transformable中的transformCoordToLocal方法。这里要说到,zrender中元素和Group都有一个构造时的初始位置,而后的所有变化都是作为transform叠加在元素上的。例如拖拽元素对应的是“原始位置 + transform”而不是一个“新位置”。

在每次变换后,Transformable中的updateTransform方法都会调用,设置自身invTransform属性为这次变化的逆矩阵。在transformCoordToLocal中对向量[x, y]应用这个逆矩阵即可得到点相对于当前形状的位置(可以理解成将点逆变换到形状变换前的位置)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
transformableProto.updateTransform = function () {
//...

m = m || matrix.create();

// ...

// 保存这个变换矩阵
this.transform = m;

// ...

this.invTransform = this.invTransform || matrix.create();
matrix.invert(this.invTransform, m);
};
// ...
transformableProto.transformCoordToLocal = function (x, y) {
var v2 = [x, y];
var invTransform = this.invTransform;
if (invTransform) {
vector.applyTransform(v2, v2, invTransform);
}
return v2;
};

综合这两个方法即可判断点是否在某元素的包围盒中。

判断contain时,首先需要满足rectContain的关系。之后根据描边和填充情况,执行contain/path下对应的containcontainStroke方法。前者实际上是后者stroke为0时的特殊情况。除了path外,可以判断点是否在元素图形内的所有元素在contain下都有对应文件。基本所有的包含都可以转化为指定闭合路径是否包含指定点的问题。

zrender利用PIP(point-in-polygon)问题winding number的解法判断点是否在path中;canvas提供的API中也有isPointInPathisPointInStroke,不过只能针对当前的path。

综上,zrender可以实现canvas内的元素和交互。

g6

g6是antv的一部分,是一个canvas实现的展示关系型数据的JS可视化库。使用canvas的原因应该也在展示大量数据和重绘上更流畅。

使用canvas实现时,g6一样会遇到zrender遇到的实现元素可交互的难题。从处理event的event.js中能看到,关联事件和元素的实现在_getEventObj处完成,剩下的步骤只是额外的封装操作:

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
Util.each(MouseEventTypes, item => {
_events.push(Util.addEventListener(el, item, ev => {
const oldEventObj = this._currentEventObj;
this._oldEventObj = oldEventObj;
this._processEventObj(ev);
const currentEventObj = this._currentEventObj;

// ...
}
}));
});

_processEventObj(ev) {
const graph = this.graph;
const canvas = graph.get('_canvas');
const frontCanvas = graph.get('_frontCanvas');
const evObj = this._getEventObj(ev, canvas);
const frontEvObj = this._getEventObj(ev, frontCanvas);

// ...
}

_getEventObj(ev, canvas) {
const graph = this.graph;
const clientX = ev.clientX;
const clientY = ev.clientY;
const canvasPoint = canvas.getPointByClient(clientX, clientY);
const point = this._parsePoint(canvasPoint.x, canvasPoint.y); // 根据pixel ratio做一个转换
const shape = canvas.getShape(canvasPoint.x, canvasPoint.y);
const item = graph.getItemByShape(shape);

// ...
// ...
}

p.s. 另说一点,frontCanvas的作用是绘制拖拽状态中的元素和辅助线等信息。

最关键的方法getPointByClientgetShape来自Graph的canvas属性,这个属性通过‘@antv/g’(G2)的canvas构造得来。

1
2
3
const G = require('@antv/g');
// ...
const Canvas = G.Canvas;

在G2中,Canvas继承自Group,可以认为Canvas本身已经扮演了根节点的角色。Canvas判断坐标对应元素的方法getShape(x,y)也来自Group。此方法遍历Group下所有元素(包括单个元素或Group),判断点[x, y]是否在范围内:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function find(children, x, y) {
let rst;
for (let i = children.length - 1; i >= 0; i--) {
const child = children[i];
if (child.__cfg.visible && child.__cfg.capture) {
// 是Group就继续向下寻找
if (child.isGroup) {
rst = child.getShape(x, y);
} else if (child.isHit(x, y)) {
rst = child;
}
}
if (rst) {
break;
}
}
return rst;
}

关键的child.isHit方法类似zrender里的contain方法。区别使用包围盒还是自身范围判断。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
isHit(x, y) {
const self = this;
const v = [ x, y, 1 ];
self.invert(v); // canvas

if (self.isHitBox()) {
const box = self.getBBox();
if (box && !Inside.box(box.minX, box.maxX, box.minY, box.maxY, v[0], v[1])) {
return false;
}
}
const clip = self.__attrs.clip;
if (clip) {
if (clip.inside(x, y)) {
return self.isPointInPath(v[0], v[1]);
}
} else {
return self.isPointInPath(v[0], v[1]);
}
return false;
}

使用包围盒时用getBBox()判断,类似zrender;否则使用isPointInPath。这点上g2不同,它只对特殊的闭合曲线如圆、矩形、贝塞尔曲线等等进行自己的实现。对一般性的path,直接使用上面提到的canvas的API来判断。

processOn

processOn严格意义上是一个产品,类似于在线的visio,编辑很流畅。使用DOM + canvas实现。具体来说:

  • DOM绘制每个元素占位,响应交互
  • canvas绘制每个DOM内的图形本身

这么做的好处在有二:1. 天然解决了元素交互的问题;2. 更平滑的元素拖拽效果。

类似的还有jsPlumb这样的使用SVG的方案,使用SVG的优势体现在交互更容易实现。

一个合法检测的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()
0%