阅读视图

发现新文章,点击刷新页面。
🔲 ☆

从一个简单功能的实现,谈谈 react 中的逻辑复用进化过程

需求:我们现在有一个获取验证码的按钮,需要在点击后禁用,并且在按钮上显示倒计时60秒才可以进行第二次点击。
本篇文章通过对这个需求的八种实现方式来讨论在 react 中的逻辑复用的进化过程

代码例子放在了 codesandbox 上。

方案一 使用 setInterval

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
import React from 'react'

export default class LoadingButtonInterval extends React.Component {
state = {
loading: false,
btnText: '获取验证码',
totalSecond: 10
}
timer = null
componentWillUnmount() {
this.clear()
}
clear = () => {
clearInterval(this.timer)

this.setState({
loading: false,
totalSecond: 10
})
}
setTime = () => {
this.timer = setInterval(() => {
const { totalSecond } = this.state
if (totalSecond <= 0) {
this.clear()
return
}
this.setState(() => ({
totalSecond: totalSecond - 1
}))
}, 1000)
}
onFetch = () => {
this.setState(() => ({ loading: true }))
const { totalSecond } = this.state
this.setState(() => ({
totalSecond: totalSecond - 1
}))
this.setTime()
}
render() {
const { loading, btnText, totalSecond } = this.state
return (
<button disabled={loading} onClick={this.onFetch}>
{!loading ? btnText : `请等待${totalSecond}秒..`}
</button>
)
}
}


方案二 使用 setTimeout

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
import React from 'react'

export default class LoadingButton extends React.Component {
state = {
loading: false,
btnText: '获取验证码',
totalSecond: 60
}
timer = null

componentWillUnmount() {
this.clear()
}
clear = () => {
clearTimeout(this.timer)

this.setState({
loading: false,
totalSecond: 60
})
}
setTime = () => {
const { totalSecond } = this.state
if (totalSecond <= 0) {
this.clear()
return
}
this.setState({
totalSecond: totalSecond - 1
})
this.timer = setTimeout(() => {
this.setTime()
}, 1000)
}
onFetch = () => {
this.setState(() => ({ loading: true }))
this.setTime()
}
render() {
const { loading, btnText, totalSecond } = this.state
return (
<button disabled={loading} onClick={this.onFetch}>
{!loading ? btnText : `请等待${totalSecond}秒..`}
</button>
)
}
}

我们可能很快就写出来两个这样的组件。使用 setTimeout 还是 setInterval 区别不是特别大。 但是我会更推荐 setTimeout 因为 万物皆递归(逃)

不过,又有更高的要求了。可以看到刚刚我们的获取验证码。如果说再有一个页面有相同的需求,只能将组件完全再拷贝一遍。这肯定不合适嘛。

那咋办嘛?

方案三 参数提取到 Props 1

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
import React from "react";
class LoadingButtonProps extends React.Component {
constructor(props) {
super(props);
this.initState = {
loading: false,
btnText: this.props.btnText || "获取验证码",
totalSecond: this.props.totalSecond || 60
};
this.state = { ...this.initState };
}
timer = null;
componentWillUnmount() {
this.clear();
}
clear = () => {
clearTimeout(this.timer);
this.setState({
...this.initState
});
};
setTime = () => {
const { totalSecond } = this.state;
if (totalSecond <= 0) {
this.clear();
return;
}
this.setState({
totalSecond: totalSecond - 1
});
this.timer = setTimeout(() => {
this.setTime();
}, 1000);
};
onFetch = () => {
const { loading } = this.state;
if (loading) return;
this.setState(() => ({ loading: true }));
this.setTime();
};
render() {
const { loading, btnText, totalSecond } = this.state;
return (
<button disabled={loading} onClick={this.onFetch}>
{!loading ? btnText : `请等待${totalSecond}秒..`}
</button>
);
}
}

class LoadingButtonProps1 extends React.Component {
render() {
return <LoadingButtonProps btnText={"获取验证码1"} totalSecond={10} />;
}
}
class LoadingButtonProps2 extends React.Component {
render() {
return <LoadingButtonProps btnText={"获取验证码2"} totalSecond={20} />;
}
}

export default () => (
<div>
<LoadingButtonProps1 />
<LoadingButtonProps2 />
</div>
);

对于上面的需求,不就是复用嘛,看我 props 提取到公共父组件一把梭搞定!
想想好像还挺美的。。

结果这时候需求变更来了:

第一点:两个地方获取验证码的api不一样。第二点:我需要在获取验证码之前做一些别的事情

挠了挠头,那咋办嘛?

方案四 参数提取到 Props 2

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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
import React from 'react'

class LoadingButtonProps extends React.Component {
// static defaultProps = {
// loading: false,
// btnText: '获取验证码',
// totalSecond: 10,
// onStart: () => {},
// onTimeChange: () => {},
// onReset: () => {}
// }
timer = null
componentWillUnmount() {
this.clear()
}
clear = () => {
clearTimeout(this.timer)
this.props.onReset()
}
setTime = () => {
const { totalSecond } = this.props
console.error(totalSecond)
if (this.props.totalSecond <= 0) {
this.clear()
return
}
this.props.onTimeChange()
this.timer = setTimeout(() => {
this.setTime()
}, 1000)
}
onFetch = () => {
if (this.loading) return
this.setTime()
this.props.onStart()
}
render() {
return <div onClick={this.onFetch}>{this.props.children}</div>
}
}

class LoadingButtonProps1 extends React.Component {
totalSecond = 10
state = {
loading: false,
btnText: '获取验证码1',
totalSecond: this.totalSecond
}
onTimeChange = () => {
const { totalSecond } = this.state
this.setState(() => ({ totalSecond: totalSecond - 1 }))
}
onReset = () => {
this.setState({
loading: false,
totalSecond: this.totalSecond
})
}
onStart = () => {
this.setState(() => ({ loading: true }))
}
render() {
const { loading, btnText, totalSecond } = this.state
return (
<LoadingButtonProps
loading={loading}
totalSecond={totalSecond}
onStart={this.onStart}
onTimeChange={this.onTimeChange}
onReset={this.onReset}
>
<button disabled={loading}>
{!loading ? btnText : `请等待${totalSecond}秒..`}
</button>
</LoadingButtonProps>
)
}
}
class LoadingButtonProps2 extends React.Component {
totalSecond = 15
state = {
loading: false,
btnText: '获取验证码2',
totalSecond: this.totalSecond
}
onTimeChange = () => {
const { totalSecond } = this.state
this.setState(() => ({ totalSecond: totalSecond - 1 }))
}
onReset = () => {
this.setState({
loading: false,
totalSecond: this.totalSecond
})
}
onStart = () => {
this.setState(() => ({ loading: true }))
}
render() {
const { loading, btnText, totalSecond } = this.state
return (
<LoadingButtonProps
loading={loading}
totalSecond={totalSecond}
onStart={this.onStart}
onTimeChange={this.onTimeChange}
onReset={this.onReset}
>
<button disabled={loading}>
{!loading ? btnText : `请等待${totalSecond}秒..`}
</button>
</LoadingButtonProps>
)
}
}
export default () => (
<div>
<LoadingButtonProps1 />
<LoadingButtonProps2 />
</div>
)

嗯?等等。。所以说这样的操作只共用了时间递归减少的部分吧?好像重复代码有点多哇,感觉和老版本也没什么太大的区别嘛。

那咋办嘛?

方案五 试试 HOC

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
84
85
import React from 'react'

function loadingButtonHoc(WrappedComponent, initState) {
return class extends React.Component {
constructor(props) {
super(props)
this.initState = initState || {
loading: false,
btnText: '获取验证码',
totalSecond: 60
}
this.state = { ...this.initState }
}
timer = null

componentWillUnmount() {
this.clear()
}
clear = () => {
clearTimeout(this.timer)

this.setState({
...this.initState
})
}
setTime = () => {
const { totalSecond } = this.state
if (totalSecond <= 0) {
this.clear()
return
}
this.setState({
totalSecond: totalSecond - 1
})
this.timer = setTimeout(() => {
this.setTime()
}, 1000)
}
onFetch = () => {
const { loading } = this.state
if (loading) return
this.setState(() => ({ loading: true }))
this.setTime()
}
render() {
const { loading, btnText, totalSecond } = this.state
return (
<WrappedComponent
{...this.props}
onClick={this.onFetch}
loading={loading}
btnText={btnText}
totalSecond={totalSecond}
/>
)
}
}
}
class LoadingButtonHocComponent extends React.Component {
render() {
const { loading, btnText, totalSecond, onClick } = this.props
return (
<button disabled={loading} onClick={onClick}>
{!loading ? btnText : `请等待${totalSecond}秒..`}
</button>
)
}
}
const LoadingButtonHocComponent1 = loadingButtonHoc(LoadingButtonHocComponent, {
loading: false,
btnText: '获取验证码Hoc1',
totalSecond: 20
})
const LoadingButtonHocComponent2 = loadingButtonHoc(LoadingButtonHocComponent, {
loading: false,
btnText: '获取验证码Hoc2',
totalSecond: 12
})
export default () => (
<div>
<LoadingButtonHocComponent1 />
<LoadingButtonHocComponent2 />
</div>
)

我们使用 高阶组件再次重写了整个逻辑。好像基本上需求都满足了?
这个地方思路在于,将 onClick 或者叫做 onStart 事件暴露出来了,最终的执行,
都是由外部组件自行决定执行时机,那么其实不管怎么搞都可以了

方案六 renderProps

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
84
85
86

import React from 'react'
class LoadingButtonRenderProps extends React.Component {
constructor(props) {
super(props)
this.initState = {
loading: false,
btnText: this.props.btnText || '获取验证码',
totalSecond: this.props.totalSecond || 60
}
this.state = { ...this.initState }
}
timer = null

componentWillUnmount() {
this.clear()
}
clear = () => {
clearTimeout(this.timer)

this.setState({
...this.initState
})
}
setTime = () => {
const { totalSecond } = this.state
if (totalSecond <= 0) {
this.clear()
return
}
this.setState({
totalSecond: totalSecond - 1
})
this.timer = setTimeout(() => {
this.setTime()
}, 1000)
}
onFetch = () => {
const { loading } = this.state
if (loading) return
this.setState(() => ({ loading: true }))
this.setTime()
}
render() {
const { loading, btnText, totalSecond } = this.state
return this.props.children({
onClick: this.onFetch,
loading: loading,
btnText: btnText,
totalSecond: totalSecond
})
}
}
class LoadingButtonRenderProps1 extends React.Component {
render() {
return (
<LoadingButtonRenderProps btnText={'获取验证码RP1'} totalSecond={15}>
{({ loading, btnText, totalSecond, onClick }) => (
<button disabled={loading} onClick={onClick}>
{!loading ? btnText : `请等待${totalSecond}秒..`}
</button>
)}
</LoadingButtonRenderProps>
)
}
}
class LoadingButtonRenderProps2 extends React.Component {
render() {
return (
<LoadingButtonRenderProps btnText={'获取验证码RP1'} totalSecond={8}>
{({ loading, btnText, totalSecond, onClick }) => (
<button disabled={loading} onClick={onClick}>
{!loading ? btnText : `请等待${totalSecond}秒..`}
</button>
)}
</LoadingButtonRenderProps>
)
}
}

export default () => (
<div>
<LoadingButtonRenderProps1 />
<LoadingButtonRenderProps2 />
</div>
)

嘿嘿,我们使用了 render Props 重写了在 Hoc 上实现的功能。个人角度看,其实比Hoc 会简洁也优雅很多!

方案七 React Hooks

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
import React, { useState, useEffect, useRef, useCallback } from 'react'
function LoadingButtonHooks(props) {
const timeRef = useRef(null)
const [loading, setLoading] = useState(props.loading)
const [btnText, setBtnText] = useState(props.btnText)
const [totalSecond, setTotalSecond] = useState(props.totalSecond)
const countRef = useRef(totalSecond)
const clear = useCallback(() => {
clearTimeout(timeRef.current)
setLoading(false)
setTotalSecond(props.totalSecond)
countRef.current = props.totalSecond
})
const setTime = useCallback(() => {
if (countRef.current <= 0) {
clear()
return
}
countRef.current = countRef.current - 1
setTotalSecond(countRef.current)

timeRef.current = setTimeout(() => {
setTime()
}, 1000)
})
const onStart = useCallback(() => {
if (loading) return
countRef.current = totalSecond
setLoading(true)
setTime()
})

useEffect(() => {
return () => {
clearTimeout(timeRef.current)
}
}, [])
return (
<button disabled={loading} onClick={onStart}>
{!loading ? btnText : `请等待${totalSecond}秒..`}
</button>
)
}
LoadingButtonHooks.defaultProps = {
loading: false,
btnText: '获取验证码',
totalSecond: 10
}
export default () => (
<div>
<LoadingButtonHooks
loading={false}
btnText={'获取验证码hooks1'}
totalSecond={10}
/>
<LoadingButtonHooks
loading={false}
btnText={'获取验证码hooks2'}
totalSecond={11}
/>
</div>
)

我们使用 hooks 重写了整个程序, 它让我们把ui和状态更明确的区分开,也去解决了一些 renderProps 在多层嵌套时的jsx 嵌套地狱问题, 当然个人感觉在这个例子上好像 Hooks 与 renderProps 版本是差别不大的。

方案八 uesHooks

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
import React, { useState, useEffect, useRef, useCallback } from 'react'
function useLoadingTimer(initState) {
const timeRef = useRef(null)
const [loading, setLoading] = useState(initState.loading)
const [btnText, setBtnText] = useState(initState.btnText)
const [totalSecond, setTotalSecond] = useState(initState.totalSecond)
const countRef = useRef(totalSecond)
const clear = useCallback(() => {
clearTimeout(timeRef.current)
setLoading(false)
setTotalSecond(initState.totalSecond)
countRef.current = initState.totalSecond
})
const setTime = useCallback(() => {
if (countRef.current <= 0) {
clear()
return
}
countRef.current = countRef.current - 1
setTotalSecond(countRef.current)

timeRef.current = setTimeout(() => {
setTime()
}, 1000)
})
const onStart = useCallback(() => {
if (loading) return
countRef.current = totalSecond
setLoading(true)
setTime()
})

useEffect(() => {
return () => {
clearTimeout(timeRef.current)
}
}, [])
return {
onStart,
loading,
totalSecond,
btnText
}
}
const LoadingButtonHooks1 = () => {
const { onStart, loading, totalSecond, btnText } = useLoadingTimer({
loading: false,
btnText: '获取验证码UseHooks1',
totalSecond: 10
})
return (
<button disabled={loading} onClick={onStart}>
{!loading ? btnText : `请等待${totalSecond}秒..`}
</button>
)
}
const LoadingButtonHooks2 = () => {
const { onStart, loading, totalSecond, btnText } = useLoadingTimer({
loading: false,
btnText: '获取验证码UseHooks2',
totalSecond: 10
})
return (
<button disabled={loading} onClick={onStart}>
{!loading ? btnText : `请等待${totalSecond}秒..`}
</button>
)
}
export default () => (
<div>
<LoadingButtonHooks1 />
<LoadingButtonHooks2 />
</div>
)

当然,更解耦的做法是,把 hooks 完全独立的提取出来成 useHooks ,最后我们再编写组件去组合 uesHooks。

在上述的例子中我们在 react 中用了 8 种 不同的方案,去描述了同一个功能的编写过程。有一点 “回” 字的多种写法的意味。不过他也代表着 react 社区在选择实现上的思想的变化过程,我觉得谈不上某一个方案,一定就完全比另外一个好。社区也有比如 HOC vs renderProps 的很多讨论。

仅以此希望大家能够辩证的去看这个过程,也希望能够在大家编写 React 组件时带来更多的新思路。

参考链接:

🔲 ⭐

使用 generic-pool 优化 puppeteer 并发问题

这个篇文章产生时间应该是在一年前的。。由于最近组里进了很多新小伙伴,写下这篇文章算是补一个介绍吧。

在17年的 D2 百度的小姐姐分享的话题 《打造前端复杂应用》时有提到利用服务端产生图片来导出 脑图和 h5 图片的问题,正好那段时间也正在做这个方向的探索 于是有 《一次canvas中文字转化成图片后清晰度丢失的探索 》这篇文章的产生。里面提到了 在之前 我使用了 phantomjs 来解决服务端页面渲染的问题。当然后面我们改成了 puppeteer。由于其实都是虚拟浏览器,两者都遇到了浏览器复用的问题。

背景

首先 对于 puppeteer 到底是一个什么样的工具在这里我不过过多的赘述。你就把他当成一个可以在服务端无界面情况下运行的一个完整 chrome 就行了。我们可以利用他模拟用户在浏览器上的几乎所有操作。当然也包括网页渲染 和截图。
比如我之前写的  geek-time-topdf  之前基于 puppeteer 实现的一个 node.js cli 工具,可以将你购买的极客时间课程打印成 PDF (由于极客时间网页版现在已经挺好用 ,并且改版,现已经没维护了。不过还是可以参考,这里只是说一下可以这么用)

我们现在其实就是利用 puppeteer + node.js 构建了一个 http 服务。那么必然我们不可能每一次请求都去产生一个 puppeteer 实例。(来一个请求就打开一个chrome。这本身就是一个非常消耗性能的行为。(ps:想象一下你在电脑上点开的每一个链接都会打开一个新的浏览器。用完然后你又把它关掉。如此往复))。当然你本身也做不到。因为当你 启动了一定数量的 puppeteer 实例之后 ,自己就报 EventEmitter 达到上限的错了。
当然你可能还是无法避免的想要启动更多实例怎么办呢?

1
2
const { EventEmitter } = require('events')
EventEmitter.defaultMaxListeners = 30 // 修改 EventEmitter 的上限

使用 链接池

好了上面废话了那么多,进入正题。 既然我们说了那么多 不可能每一次都启动和关闭一个 puppeteer 实例。 那么今天我们的主角  generic-pool  就要出场了。

这是一个基于 Promise 的通用链接池库。有了他之后我们就可以 将 puppeteer 实例放在我们的链接池中,如果有请求进来,那么就去池子里面去取一个实例。我们可以设置实例的上限,和常驻池中的实例数量。(一个任务队列,超过上限时自动排队。)然后你拿到这个实例之后就可以去进行和普通创建实例一样的操作了。(性能对比图这里就不给出了,提升还是非常巨大的,可以自行尝试。)

具体的使用可以在 github 查看这里就不多聊了。我们直接基于我们目前的一个启动创建配置来进行一个讲解。(算了,讲解就直接写在代码注释里了。) -_-!

puppeteer-pool.js

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
84
85
86
87
88
'use strict'
const puppeteer = require('puppeteer')
const genericPool = require('generic-pool')

/**
* 初始化一个 Puppeteer 池
* @param {Object} [options={}] 创建池的配置配置
* @param {Number} [options.max=10] 最多产生多少个 puppeteer 实例 。如果你设置它,请确保 在引用关闭时调用清理池。 pool.drain().then(()=>pool.clear())
* @param {Number} [options.min=1] 保证池中最少有多少个实例存活
* @param {Number} [options.maxUses=2048] 每一个 实例 最大可重用次数,超过后将重启实例。0表示不检验
* @param {Number} [options.testOnBorrow=2048] 在将 实例 提供给用户之前,池应该验证这些实例。
* @param {Boolean} [options.autostart=false] 是不是需要在 池 初始化时 初始化 实例
* @param {Number} [options.idleTimeoutMillis=3600000] 如果一个实例 60分钟 都没访问就关掉他
* @param {Number} [options.evictionRunIntervalMillis=180000] 每 3分钟 检查一次 实例的访问状态
* @param {Object} [options.puppeteerArgs={}] puppeteer.launch 启动的参数
* @param {Function} [options.validator=(instance)=>Promise.resolve(true))] 用户自定义校验 参数是 取到的一个实例
* @param {Object} [options.otherConfig={}] 剩余的其他参数 // For all opts, see opts at https://github.com/coopernurse/node-pool#createpool
* @return {Object} pool
*/
const initPuppeteerPool = (options = {}) => {
const {
max = 10,
min = 2,
maxUses = 2048,
testOnBorrow = true,
autostart = false,
idleTimeoutMillis = 3600000,
evictionRunIntervalMillis = 180000,
puppeteerArgs = {},
validator = () => Promise.resolve(true),
...otherConfig
} = options

const factory = {
create: () =>
puppeteer.launch(puppeteerArgs).then(instance => {
// 创建一个 puppeteer 实例 ,并且初始化使用次数为 0
instance.useCount = 0
return instance
}),
destroy: instance => {
instance.close()
},
validate: instance => {
// 执行一次自定义校验,并且校验校验 实例已使用次数。 当 返回 reject 时 表示实例不可用
return validator(instance).then(valid => Promise.resolve(valid && (maxUses <= 0 || instance.useCount < maxUses)))
}
}
const config = {
max,
min,
testOnBorrow,
autostart,
idleTimeoutMillis,
evictionRunIntervalMillis,
...otherConfig
}
const pool = genericPool.createPool(factory, config)
const genericAcquire = pool.acquire.bind(pool)
// 重写了原有池的消费实例的方法。添加一个实例使用次数的增加
pool.acquire = () =>
genericAcquire().then(instance => {
instance.useCount += 1
return instance
})
pool.use = fn => {
let resource
return pool
.acquire()
.then(r => {
resource = r
return resource
})
.then(fn)
.then(
result => {
// 不管业务方使用实例成功与后都表示一下实例消费完成
pool.release(resource)
return result
},
err => {
pool.release(resource)
throw err
}
)
}
return pool
}

如何使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const pool = initPuppeteerPool({ // 全局只应该被初始化一次
puppeteerArgs: {
ignoreHTTPSErrors: true,
headless: false, // 是否启用无头模式页面
timeout: 0,
pipe: true, // 不使用 websocket
}
})
// 在业务中取出实例使用
const page = await pool.use(instance=>{
const page = await instance.newPage()
await page.goto('http://xxx.xxx', { timeout: 120000 })
// do XXX ...
return page
})
// do XXX ...

// 应该在服务重启或者关闭时执行
//pool.drain().then(() => pool.clear())

可以看到我们在 基于generic-pool 的情况下构建了一个 Puppeteer 的池。每一次请求进来之后 我们调用 pool.use 去取得一个实例。然后去进行我们后续的操作就可以了。

整体流程如下:在服务启动时启动池。
请求到达->从池中取得一个 Puppeteer 实例->打开tab页->运行代码->关闭tab页->返回数据(其他的管理都交给池了)

比如简述一下我们目前运行代码的业务流程:

  1. 拿到 json 数据把 canvas 页面渲染出来 (前端页面渲染流程,配置与渲染分离,只有在渲染的一刻才知道最终产生的数据是什么。
  2. 渲染页面与 Puppeteer 交互。拿到处理后的 json
  3. 拿到截图的配置参数
  4. 使用 Puppeteer Page api 截图。
  5. 对产生的 图片 buffer 做格式转化(调用 imagemagick(一个跨平台图像处理库) 等处理图片)
  6. 数据上传 阿里 oss
  7. 异步通知其他端处理已经结束。

然后我们再仔细看配置中的 maxUses 可以看到我们自定义扩展了每一个 Puppeteer  最多可以被使用的次数(防止实例变卡什么的)来防止一些意外情况出现。

其实我们之所以需要一个池其中一个问题主要就是处理性能问题。。这一部分其实在在业务代码中也需要处理。下面简单说几个点。

  1. Puppeteer 什么样的启动参数对服务性能有提升?
  2. 在截图时选什么样的参数能在达到业务要求的情况下尽可能的提升性能?
  3. 是产生图片在本地?还是直接拿到 图片 buffer 去和第三方服务对接?
  4. 有没有可能把业务处理流程进行步骤拆分?让 Puppeteer 承担的工作少一些?

那我们有了一个 Puppeteer 的池,实现复用 Puppeteer 实例。那么如何更好的去实现一个 http 服务呢?

结合 egg.js

egg.js 是蚂蚁金服出品的一个企业级 node.js 框架。可以高效的搭建一个可用的 http 服务,其他介绍自行官网查看。具体我这里就不多介绍了。

这里简单说一下怎么结合 puppeteer-pool 在一起使用 核心其实就是 创建 app.js  做初始化处理。

需要注意 结合 egg.js 使用时,需要手动指定 workers 数量为 1: egg-scripts start --daemon --workers=1 不然会启动 pool.max * workers 数量的 Puppeteer 实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const initPuppeteerPool = require('./util/puppeteer-pool')
const { EventEmitter } = require('events')
EventEmitter.defaultMaxListeners = 30
class AppBootHook {
constructor(app) {
this.app = app
}
async didLoad() {
// 所有的配置已经加载完毕
// 可以用来加载应用自定义的文件,启动自定义的服务
this.app.pool = initPuppeteerPool()
}
async beforeClose() {
if (this.app.pool.drain) {
await this.app.pool.drain().then(() => this.app.pool.clear())
}
}
}
module.exports = AppBootHook

server.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const Service = require('egg').Service
class ScreenshotService extends Service {
renderPage(url) {
const { app, config } = this
const imageBuffer = await app.pool.use(async instance => {
// 1. 创建一个新窗口
const page = await instance.newPage()
await page.goto(url, { timeout: 120000 })
// 2. 截图参数,截图
const buf = await page.screenshot({ ...imgOutOption, clip: opt })
return buf
})
return imageBuffer
}
}

说一下 Puppeteer 使用到的坑

这里说三个 Puppeteer 使用上的坑吧:

  1. 可以看到第 5 点:由于我们的场景对于图片清晰度要求很高,所以发现了这个问题。(Puppeteer 导出 png 再调用 imagemagick 转成jpg ,也比直接使用 Puppeteer 导出 jpg 清晰度高(即便清晰度设置成了100 -_- !))
  2. Puppeteer 无法截图产生超过 65535 的图。(当然 imagemagick,sharp 也无法处理超过这个的图。(这个是一个挺有意思的事情。有兴趣的可以去搜索这个数看看
  3. Puppeteer@1.12.2 之后的版本 单张截图超过 8000(4096 * 2)(不确定值,但是确实会出问题,因为出现问题就没升级了)有一定概率导致 图片部分区域为空白。

后续/扩展

这是目前未处理的部分。

其实可以看到我们在上面的处理实现了多个 Puppeteer 实例的复用,但是其中也有一个问题,那就是其实我们在这样的情况下使用每一次请求过来只会利用一个 浏览器 窗口,那么我们的 QPS 直接与我们新建的 Puppeteer 实例上限挂钩(配置中的 max 属性),当然还有单个任务的处理时间。(当然在我们内部的业务场景没啥问题(长度过长,图片太多。然后还要处理图片。cpu早100%了)

能不能在实例池的基础上,再创建一个单实例的窗口池呢? (因为实际上我们真正操作的内容 其实都是 Puppeteer 的 Page )这部分是还没做的,就交给你们去实现了

参考链接:

🔲 ⭐

一个使用 react 的思想去使用 vue 的方式

有一个 react 开发者 问我 vue 如何上手开发?然后我是这么和他描述的。。
用 react 的思想 去考虑 vue 要怎么写。
本文很水。而且将来感觉一定会被打脸,期待那一天到来。

其实也就几块

  1. 你可以把 template 就看成 react 的 render 就是写法有一些不一样,看一下 vue 的指令就可以了,当然也可以直接用 render function
  2. data 的话 和 react 的 state 也没什么区别 , 只不过赋值方式变成了 this.setState({text:’state’}) this.text = ‘state’ (vue 有一个坑,数组里面 data: [{a:1}] 使用 this.data[0].b = 1 => data”:[{a:1,b:1}] 新增了一个b字段 这样是不会刷新页面的。react 不会有这个问题(至于怎么刷新,这个就先你自己去看文档把哈哈哈(底层实现不考虑
  3. methods 对应的就是 react 中 class componnet 中 直接写上去的方法 onMenuClick = () => {} 这种 (vue里面不需要箭头函数
  4. 事件监听的话 @click=”onMenuClick” 和 react 中的 onClick={this.onMenuClick} 也没什么区别
  5. 生命周期 mounted 和 react 中的 componentDidMount 也基本一致(周期里面就这个最重要了 当然还有 wtach需要看一下
  6. computed 正确的使用方式,其实就是一个纯函数,在里面写有副作用的内容,对可维护性是一个灾难(自动计算,实际上不用也没关系 (react里面就没有
  7. 其他的 就是 props components 这两个了,用法是一样的,但是得手动声明一下,按照文档来就可以
  8. 路由,vue 的路由其实更好理解,react-route@4 + react-route-config 使用上基本也一致。 只不过 router-view 换成了 renderRoutes
  9. 剩下的部分 其实就照着文档看看就行拉 https://cn.vuejs.org/v2/api/#components

然后在组内问了一下大家的看法

Q:感觉写vue和react的思维方式不太一样?
A:大的说的话,一个是函数式,一个是响应式。

Q:vue比较符合常人的思维,比较好上手。同时意味着,不如react的方法抽象。watch就很有意思。
A:watch 是一个双刃剑吧。确实很方便…不过也容易写出来不易读或者性能很差的代码。

Q:React 的生命周期component WillReceiveProps可以拿到nextProps 这样的参数,父组件参数改变时,子组件方便监听并特殊处理。vue 中通过computed 监听数据变化并处理,感觉怪怪的
A:其实 componentWillReceiveProps 即将被废弃了。。。不过这个需求确实是有的 ,也就是父组件和 子组件其实都有一份state,并且父组件的状态更改会影响 子组件的state 更改的情况。 vue里面应该这个用 watch 会比较多,结合上面,其实 还是建议 computed 使用 纯函数,如果是纯函数的话,那么就不能去改子组件的 state 了 (state 改成 data 也是一样。

Q:Vue 通过 getter/setter 以及一些函数的劫持,能精确知道数据变化,不需要特别的优化就能达到的性能
React 默认是通过比较引用的方式进行的,如果不优化(PureComponent/shouldComponentUpdate)可能导致大量不必要的VDOM的重新渲染
A:嗯…有点偏题。 一个新人学 vue 进来应该不会考虑这个?不过基本没错

Q:computed是实现y = f(x) ,因变量只能通过这个函数得到才能用 computed吧,否则应该是有问题的。
A:虽然是这么说,但是你看了主站的代码就知道,这个其实不是一个强绑定。主站里面 computed 里面做有副作用的事情,也有好多- -,不是一个好的实践。(我上面说的是建议只当成纯函数来用

下一篇,

《在 vue 中使用 jsx 与 class component 的各种姿势》

🔲 ⭐

从一个简单的实例看 JavaScript 的异步编程进化历

回调地狱

很久没有进行过创作了,也感觉到了自己的不足。这一篇文章是对于 JavaScript 异步编程的一个 整理
希望自己更多的成为一个创造者,而不是只会看,会用,还需要深入理解到原理吧。

例子如下:

我们有 A, B, C, D 四个请求获取数据的函数(函数自己实现),
C 依赖 B 的结果,D 依赖 ABC 的结果,最终输出 D 的结果。

版本一

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
// 伪代码
function A(callbak) {
ajax(url, function(res) {
callbak(res);
});
}
function B(callbak) {
ajax(url, function(res) {
callbak(res);
});
}
function C(data, callback) {
ajax(url, data, function(res) {
callbak(res);
});
}
function D(data1, data2, data3, callback) {
ajax(url, { data1, data2, data3 }, function(res) {
callbak(res);
});
}

A(function(resa) {
B(function(resb) {
C(resb, function(resc) {
D(resa, resb, resc, function(resd) {
console.log("this is D result:", resd);
});
});
});
});

emm…代码还是能运行,但是写法丑陋,回调地狱,如果还有请求依赖,得继续回调嵌套
性能太差,没有考虑 A 和 B 实际上是可以并发的。

例子二

函数基础实现如同例子一,但是考虑 A,B 可以并发的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 伪代码
let resa = null;
let timer = null;

A(res => {
resa = res;
});

B(resb => {
C(resb, resc => {
timer = setInterval(() => {
if (resa) {
D(resa, resb, resc, resd => {
console.log("this is D result:", resd);
timer && clearInterval(timer);
});
}
}, 100);
});
});

考虑了 A,B 的并发,使用 setInterval 轮询实现,并不一定实时。性能太差。

例子三

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
// 伪代码
let count = 2;
let resa = null;
let resb = null;
let resc = null;
function done() {
count--;
if (count === 0) {
D(resa, resb, resc, resd => {
console.log("this is D result:", resd);
});
}
}

A(res => {
resa = res;
done();
});
B(datab => {
C(datab, datac => {
resb = datab;
resc = datac;
done();
});
});

使用 计数器实现。性能没什么问题,但是 封装太差,写法恶心。

例子四

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
// 实现并发
function parallel(tasks, callback) {
let count = tasks.length;
let all = [];
tasks.forEach((fn, index) => {
fn(res => {
all[index] = res;
count--;
if (count === 0) {
callback(all);
}
});
});
}
// 实现串行
function waterfall(tasks, callback) {
let count = tasks.length;
function loop(...args) {
let task = tasks.shift();
task.apply(
null,
args.concat([
(...res) => {
count--;
if (count === 0) {
return callback(res);
}
loop(...res);
}
])
);
}
loop();
}
function A(cb = () => {}) {
setTimeout(() => {
cb("a");
}, 2000);
}
function B(cb = () => {}) {
setTimeout(() => {
cb("b");
}, 1000);
}
function C(datab, cb = () => {}) {
setTimeout(() => {
cb(datab, "c");
}, 1000);
}
function D(data, datab, datac, cb = () => {}) {
cb("d");
}
parallel(
[
A,
cb => {
waterfall([B, C], (datab, datac) => {
cb(datab, datac);
});
}
],
data => {
const [resa, [resb, resc]] = data;
D(resa, resb, resc, resd => {
console.log("this is D result:", resd);
});
}
);

模仿 async.js 提炼出来了 waterfall,parallel,两个流程控制函数。还不错。
但是写法还是麻烦,对于 A,B,C 的实现有要求。得自己考虑好每一次 callback 的值。

async.js 是我认为在目前 JavaScript callback 的终极解决方案了(没用过 fib.js..

推荐查看 github async.js 源码。

waterfall 可以考虑使用函数式的形式实现:

1
2
3
4
5
6
7
8
9
10
function pipe(...fnList) {
return function(...args) {
const fn = fnList.reduceRight(function(a, b) {
return function(...subArgs) {
return b.apply(this, [].concat(subArgs, a));
};
});
return fn.apply(this, args);
};
}

例子五

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function A() {
return fetch("http://google.com");
}
function B() {}
function C() {}
function D() {}

Promise.all[(A(), B().then(b => C(b)))]
.then(([resa,{resb,resc}) => {
return D(resa,resb,resc);
})
.then(resd => {
console.log("this is D result:", resd);
});

使用 Promise 来代替 之前的 callback。好评。
用 Promise.all 来控制并发,使用 .then 串行请求,整体看起来非常舒服了,脱离了回调地狱。

例子六

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
function A(cb) {
setTimeout(() => {
cb("a");
}, 2000);
}
function B(cb) {
setTimeout(() => {
cb("b");
}, 1000);
}
function C(datab, cb) {
setTimeout(() => {
cb("c");
}, 1000);
}
function D(dataa, datab, datac, cb) {
setTimeout(() => {
cb("d");
}, 1000);
}
function thunk(fn) {
return function(...args) {
return function(callback) {
fn.call(this, ...args, callback);
};
};
}
function scheduler(fn) {
var gen = fn();

function next(data) {
var result = gen.next(data);
if (result.done) return;
// 如果没结束就继续执行
result.value(next);
}

next();
}

// generator 实际代码
function* generatorTask() {
const resa = yield thunk(A)();
const resb = yield thunk(B)();
const resc = yield thunk(C)(resb);
const resd = yield thunk(D)(resa, resb, resc);
console.log("this is D result:", resd);
return null;
}

scheduler(generatorTask);

使用 generator + callback 来控制流程顺序,还是同步写法,看起来还是挺牛逼的。
但是 generator 不会自动执行,需要自己手动写一个执行器,并且依赖于 thunk 函数。麻烦!
等等。。又全变成了串行?垃圾

例子七

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
function A() {
return new Promise(r =>
setTimeout(() => {
r("a");
}, 2000)
);
}
function B() {
return new Promise(r =>
setTimeout(() => {
r("b");
}, 1000)
);
}
function C(datab) {
return new Promise(r =>
setTimeout(() => {
r("c");
}, 1000)
);
}
function D(dataa, datab, datac) {
return new Promise(r =>
setTimeout(() => {
r("d");
}, 1000)
);
}
function scheduler(fn) {
var gen = fn();

function next(data) {
var result = gen.next(data);
if (result.done) return;
// 如果没结束就继续执行
result.value.then(next);
}

next();
}

// generator 实际代码
function* generatorTask() {
const [resa, { resb, resc }] = yield Promise.all([
A(),
B().then(resb => C(resb).then(resc => ({ resb, resc })))
]);
const resd = yield D(resa, resb, resc);
console.log("this is D result:", resa, resb, resc, resd);
return resd;
}

scheduler(generatorTask);

抛弃了 thunk 函数,修改了一下 A,B,C,D。的实现以及 generator 执行函数 scheduler。 结合了 Promise 重新实现了并发和串行。
再等等??好麻烦啊。。然后并发好像和 generator 没什么关系吧。果然还是 Promise 大法好。

关于 generator 的自动执行建议直接看 github tj/co 的源码。

例子八

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function A() {
return fetch("http://google.com");
}
// ...B,C,D
async function asyncTask() {
const resa = await A();
const resb = await B();
const resc = await C(resb);
const resd = await D(resa, resb, resc);
return resd;
}

asyncTask().then(resd => {
console.log("this is D result:", resd);
});

使用 Promise 结合 async/await 的形式 ,看起来非常简洁。也不用自己写执行器了,舒服。
但是和上面有几个版本出现了一样的问题,没有考虑并发的情况,导致性能下降。

例子九,终极方案?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ...B,C,D
async function asyncBC() {
const resb = await B();
const resc = await c(resb);
return { resb, resc };
}
async function asyncTask() {
// const [resa,{resb,resc}] = await Promise.all([A(), B().then(resb=>C(resb)]);
const [resa, { resb, resc }] = await Promise.all([A(), asyncBC()]);
const resd = await D(resa, resb, resc);
return resd;
}
asyncTask().then(resd => {
console.log("this is D result:", resd);
});

使用 Promise.all 结合 async/await 的形式,考虑了并发和串行,写法简洁。
应该算是目前的终极方案了。 async/await 作为 generator 语法糖还是非常的甜的。

例子十 使用 RxJs

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
import { defer, forkJoin } from "rxjs";
import { mergeMap, map } from "rxjs/operators";

function A() {
return fetch("https://cnodejs.org/api/v1/topics").then(res => res.json());
}
function B() {
return fetch("https://cnodejs.org/api/v1/topics").then(res => res.json());
}
function C() {
return fetch("https://cnodejs.org/api/v1/topics").then(res => res.json());
}
function D(...args) {
return fetch("https://cnodejs.org/api/v1/topics")
.then(res => res.json())
.then(res => [...args, res]);
}
// A, B, C, D 函数必须返回 Promise
// 使用 defer 产生一个 Observable
const A$ = defer(() => A());
// pipe 类型 Promise 链中 的 then
const BC$ = defer(() => B()).pipe(
// mergeMap 映射成 promise 并发出结果
mergeMap(resB => {
// 使用 map 产生新值
return defer(() => C(resB)).pipe(map(resC => [resB, resC]));
})
);

// forkJoin 类似 Promise.all 并发执行多个 Observable
forkJoin(A$, BC$)
.pipe(mergeMap(([resa, [resb, resc]]) => D(resa, resb, resc)))
.subscribe(resd => {
console.log("this is D result:", resd); // <------- fnD 返回的结果
});

使用 rxjs 来构建流式的请求过程。结构还是非常清晰的,但是相对繁琐,概念也比 原生的 Promise 和 await 要多

不过 rxjs 操作符巨多,掌握之后,可以做更多的事情


结语:

从上面几个例子我们可以窥探到 JavaScript 对于异步编程体验的一个非常大的进步。

但是同时我们其实可以看到不论是 generator 还是 async/await。其实更多的是基于 Promise 之上的一些语法简化。
没有从 callback 过渡到 Promise 的时候那种真正心灵上的愉悦。

感谢 @墨水 之前在内部分享提供的 demo 版本。

🔲 ⭐

中文播客推荐

从大学时期听书开始,慢慢接触到更多的播客。听一些人讲故事,讲技术,感觉也是一个非常不错的了解世界的途径。已经成为我生活中的一部分。不过从身边感觉到播客还是比较小众的。于是想推荐一下我在听的一些播客吧。
引用一句《内核恐慌》的话作为推荐语:“我们虽然号称 Hacker ,但是也没有干货,想听的人就听,不想听的人就别听。”

推荐播客客户端: Moon FM,播客,小宇宙,player.fm

IT 技术主题

《内核恐慌》

内核恐慌

《内核恐慌》(Kernel Panic) 是吴涛和 Rio 做的播客,首播于 2014 年 10 月。号称硬核,可也没什么干货。想听的人听,不想听的人就别听。

rss

推荐:

  • 类型系统
  • 并发与异步
  • 数学与编程

《Teahour.fm》

Teahour.fm

Teahour.fm 专注程序员感兴趣的话题,包括 Web 设计和开发,移动应用设计和开发,创业以及一切 Geek 的话题。

rss

推荐:

  • 和 PingCAP CTO 畅谈数据库和编程语言(rust & go
  • 和 Vue.js 框架的作者聊聊前端框架开发背后的故事
  • 与百姓网架构师艾芙聊职业发展和工程师文化

《代码时间》【已停更】

代码时间

代码时间是一个面向程序员的中文播客节目, 致力于通过语音的方式传播程序员的正能量。 节目的shownotes请移步节目主页。

rss

推荐:

  • ES2015(上) - 贺师俊
  • ES2015(下) - 贺师俊
  • Clojure编程语言 – Loretta

ggtalk

接地气、有价值的闲聊节目。一帮程序员,在无尽的接需求写代码改 bug 加班上线循环中开辟出来的一块空地,想想过去,聊聊现在,偶尔也展望一下未来。
头发越来越少,经验越来越多;颈椎开始僵硬,头脑依然灵活。代码写多了就想尝试点新东西,聊技术,聊工作,聊生活。挤地铁?又堵车?随便点一期吧,听个乐呵。

rss

推荐:

  • 聊聊跑步这件小事
  • 游戏加速纵横谈

商业科技相关

《疯投圈》

疯投圈

《疯投圈》是一档为创业者、投资人、分析师,以及任何对创业、投资有兴趣的人准备的播客节目。每期节目我们为你深度解剖创投行业新动向。

rss

推荐:

  • 再谈出海电商的全球机遇
  • 复杂服务行业如何平台化
  • 拼多多=中国的 Costco ?

比特新声

比特新声

《比特新声》是由郝海龙和有才主持的中文科技类播客。在节目中,我们会尽量避免不加解释地使用过于抽象的科技术语,力争让每一个有独立思考能力的人听懂我们的节目。我们坚信凡实验性的东西都有一种独特的魅力,好奇心是第一生产力,同时希望用不同的观点去描述我们所处的时代。

rss

推荐:

  • 一个拥有多线程超能力的开发者应该是什么样的?

迟早更新

迟早更新

「迟早更新」是一档探讨科技、商业、设计和生活之间混沌关系的播客节目,也是风险基金 ONES Ventures 关于热情、趣味和好奇心的音频记录。我们希望通过这档播客,能让熟悉的事物变得新鲜,让新鲜的事物变得熟悉。

rss

字谈字畅

《字谈字畅》是第一档用华语制作的字体排印主题播客节目,由 Eric Liu 与钱争予搭档主播。Type is Beautiful 出品。

rss

声东击西

我们聊技术和创新,也聊文化和电影,这里有一手的现场观察和体验,还有不定期出现的神秘嘉宾。你可以感受星战粉丝大会现场的沸腾,也能想象一下未来世界里的出行,以及美国年轻人都在关心什么新鲜事儿。

rss

推荐:

  • Airbnb 上篇:你不仅能住在别人家,还有人带你玩

Checked 【已停更】

以科技提升效率,用效率改变生活。

rss

推荐:

  • 日历/待办事项/GTD
  • 访谈「也谈钱」: 你的钱是可以给你赚钱的
  • Pin 开发者——钟颖访谈

《IT 公论》

IT 公论

《IT 公论》由 IPN 出品、不鸟万如一和 Rio 主持,首播于二零一三年十一月。本节目系一种综合性之科技节目。收听对象,并不限于社会上某一阶层。凡职业部门不同,知识水准互异,而对于科技有共同兴趣者,从任何角度,收听此秀,不致味同嚼蜡,毫无所得。一切题材,即就雅俗两极之范围内,伸缩去取,尽量适用多方面之需要,以求俗不伤雅,雅不背时。科技播客,非奇技淫巧之表现也,亦非粉黑二元论争也。盖科技与吾人之关系至密至切,而欲其适合各人之需要,不悖于美之真义,则软件式款,与夫工作生活之配合,用例之转换,必有相当研究方克能之。而欲吾人乐愿研究之,则对于科技之兴趣,必先有以引起之,此《IT 公论》之滥觞也。二零一六年四月停播。

rss

WEB VIEW

「不囿于 WEB,不止于 VIEW」,WEB VIEW 是由王隐和敬礼主持的一档泛科技播客。节目中我们谨慎考量技术进步所带来的优缺点,提倡用「人治」的方法重新审视我们的日常生活。

枫言枫语

听见科技与人文的声音。

看看世界

博物志

博物志

rss

灭茶苦茶

不伦不类、不易流行。了解日本是不够的,我们要活用日本。不鳥萬如一主理,IPN 出品。

rss

一天世界

一天世界,昆乱不挡。不鳥萬如一主理。IPN 出品。《一天世界》博客

海螺电台

[海螺电台] 播客是一个记录行动和探索过程的创作计划

推荐:

  • 纵使强风起,人生不言弃(箱根山岳险天下!)
  • 当我们在谈跑步时,我们再谈什么

心理学相关

得意忘形

《得意忘形》是一个主张追求个体自由与探寻真理的媒体计划。带着对生命的有限性与无目的性的敬畏,我们试图为读者与听众提供更全面的觉察自我与认知世界的工具,以不断重建当下的方式穿越时间、抵达生活的本质。

得意忘形

rss

推荐:

  • 序言:「无为」与刻意、大脑的双系统、自由主义的危机与开篇絮语
  • 网球:孤独和它所创造的

Blow Your Mind

两个人的公路播客。

rss

知识型

狗熊有话说

独立知识型播客

rss

杂项

UX Coffee 设计咖

《UX Coffee 设计咖》是一档关于用户体验的播客节目。我们邀请来自硅谷和国内的学者和职人来聊聊「产品设计」、「用户体验」和「个人成长」。微信公众号: uxcoffee

rss

太医来了

《太医来了》由 IPN 出品、由前骨科医生初洋和妇产科医生田吉顺主持,是中文互联网第一档医生谈话类播客。节目里没有老专家讲养生,只有几个医生聊聊医院里的事儿,顺便给大家做做科普。

rss

黑水公园

《黑水公园》是一档在网络平台定期播出的广播节目,以轻松的对话形式向听众普及科幻电影知识,讲述电影真实故事,并且会定期分享各类优质的科幻作品。

推荐:

  • 《宝石之国》恭喜你发现宝藏了!
  • 《浩瀚苍穹》二百年后的太空战争
  • 十分好看的《白日梦想家》
🔲 ☆

学习 Promise,掌握未来世界 JS 异步编程基础

其实想写 Promise 的使用已经很长时间了。一个是在实际编码的过程中经常用到,一个是确实有时候小伙伴们在使用时也会遇到一些问题。
Promise 也确实是 ES6 中 对于写 JS 的方式,有着真正最大影响的 API 特性之一。
本文是实际使用使用过程中的一个总结
看一下文件创建时间 2017-10-09,拖延症真是太可怕了。。。还是得增强执行力啊!不忘初心,加油吧!

前言 && 基础概念

Promise 是解决 JS 异步的一种方案,相比传统的回调函数,Promise 能解决多个回调严重嵌套的问题。

Promise 对象代表一个异步操作,有三种状态: pending、fulfilled 或 rejected ,状态的转变只能是 pending -> fulfilled 或者 pending -> rejected ,且这个过程一旦发生就不可逆转

个人认为讲解 Promise 实际上需要分成两个部分

  1. 对于 Promise 构造函数的使用说明。
  2. Promise 原型对象上的一些方法。

Promise 构造函数

ES6 规定,Promise 对象是一个构造函数,用来生成 Promise 实例。

Promise 构造函数接受一个函数作为参数,该函数的两个参数分别是 resolve 和 reject 。它们是两个函数,由 JavaScript 引擎提供,不用自己部署。

resolve 函数的作用是将 Promise 对象的状态从“未完成”变为“成功”(即从 pending 变为 fulfilled ),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;
reject 函数的作用是,将 Promise 对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected ),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。

下面代码创造了一个 Promise 实例。

1
2
3
4
5
6
7
8
9
10
function request() {
return new Promise((resolve, reject) => {
/* 异步操作成功 */
setTimeout(() => {
resolve("success");
}, 1000);
// 取消注释这里可以体现,Promise 的状态一旦变更就不会再变化的特性
// reject('error');
});
}

接收

1
2
3
4
5
6
7
request()
.then(result => {
console.info(result);
})
.catch(error => {
console.info(error);
});

上述 new Promise() 之后,除去用 catch 去捕获错误之外,也可以用 then 方法指定 resolvereject 的回调函数
也能达到捕获错误的目的。

1
2
3
4
5
6
7
8
request().then(
result => {
console.info(result);
},
error => {
console.info(error);
}
);

原型上的方法

Promise.prototype.then()

p.then(onFulfilled, onRejected);

then 方法 是定义在 Promise.prototype 上的方法,如上面的例子一样,有两个参数,fulfilled 的回调函数和 rejected 的回调函数,第二个参数时可选的。

两个关键点:

  1. then 方法的返回值是一个新的 Promise 实例,所以对于调用者而言,拿到一个 Promise 对象,调用 then 后仍然返回一个 Promise ,而它的行为与 then 中的回调函数的返回值有关。如下:
  • 如果 then 中的回调函数返回一个值,那么 then 返回的 Promise 将会成为接受状态,并且将返回的值作为接受状态的回调函数的参数值。
  • 如果 then 中的回调函数抛出一个错误,那么 then 返回的 Promise 将会成为拒绝状态,并且将抛出的错误作为拒绝状态的回调函数的参数值。
  • 如果 then 中的回调函数返回一个已经是接受状态的 Promise,那么 then 返回的 Promise 也会成为接受状态,并且将那个 Promise 的接受状态的回调函数的参数值作为该被返回的 Promise 的接受状态回调函数的参数值。
  • 如果 then 中的回调函数返回一个已经是拒绝状态的 Promise,那么 then 返回的 Promise 也会成为拒绝状态,并且将那个 Promise 的拒绝状态的回调函数的参数值作为该被返回的 Promise 的拒绝状态回调函数的参数值。
  • 如果 then 中的回调函数返回一个未定状态(pending)的 Promise,那么 then 返回 Promise 的状态也是未定的,并且它的终态与那个 Promise 的终态相同;同时,它变为终态时调用的回调函数参数与那个 Promise 变为终态时的回调函数的参数是相同的。
  1. 链式调用。把嵌套回调的代码格式转换成一种链式调用的纵向模式。

比如说回调形式: 一个回调地狱的例子

1
2
3
4
5
6
7
8
9
a(a1 => {
b(a1, b1 => {
c(b1, c1 => {
d(c1, d1 => {
console.log(d1);
});
});
});
});

这样的横向扩展可以修改成(a,b,c,d)均为返回 Promise 的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
a()
.then(b)
.then(c)
.then(d)
.then(d1 => {
console.log(d1);
});
//===== 可能上面的例子并不太好看 ===下面这样更直观
a()
.then(a1 => b(a1))
.then(b1 => c(b1))
.then(c1 => d(c1))
.then(d1 => {
console.log(d1);
});

这样的纵向结构,看上去清爽多了。

Promise.prototype.catch()

除了 then() ,在 Promise.prototype 原型链上的还有 catch() 方法,这个是拒绝的情况的处理函数。

其实 它的行为与调用 Promise.prototype.then(undefined, onRejected) 相同。 (事实上, calling obj.catch(onRejected) 内部 calls obj.then(undefined, onRejected)).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 1.
request().then(
result => {
console.info(result);
},
error => {
console.info(error);
}
);

// 2.
request()
.then(result => {
console.info(result);
})
.catch(error => {
console.info(error);
});

如上这个例子:两种方式在使用,与结果基本上是等价的,但是 仍然推荐第二种写法,下面我会给出原因:

  1. 在 Promise 链中 Promise.prototype.then(undefined, onRejected)onRejected 方法无法捕获当前 Promise 抛出的错误,而后续的 .catch 可以捕获之前的错误。
  2. 代码冗余
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
new Promise((resolve, reject) => {
setTimeout(() => {
resolve("reject");
}, 1000);
})
.then(
result => {
console.log(result + "1");
throw Error(result + "1"); // 抛出一个错误
},
error => {
console.log(error + ":1"); // 不会走到这里
}
)
.then(
result => {
console.log(result + "2");
return Promise.resolve(result + "2");
},
error => {
console.log(error + ":2");
}
);
// reject1, Error: reject1:2

如果使用 .catch 方法,代码会简化很多,这样实际上是延长了 Promise 链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
new Promise((resolve, reject) => {
setTimeout(() => {
resolve("reject");
}, 1000);
})
.then(result => {
console.log(result + "1");
throw Error(result + "1"); // 抛出一个错误
})
.then(result => {
console.log(result + "2");
return Promise.resolve(result + "2");
})
.catch(err => {
console.log(err);
});
// reject1, Error: reject1:2

Promise.prototype.finally()

暂未完全成为标准的一部分,处于:Stage 4

finally() 方法返回一个 Promise,在执行 then()catch() 后,都会执行finally指定的回调函数。(回调函数中无参数,仅仅代表 Promise 的已经结束

等同于使用 .then + .catch 延长了原有的 Promise 链的效果,避免同样的语句需要在 then()catch() 中各写一次的情况。

mdn-Promise-finally

Promise 对象上的方法

Promise.all() 用来处理 Promise 的并发

Promise.all 会将多个 Promise 实例封装成一个新的 Promise 实例,新的 promise 的状态取决于多个 Promise 实例的状态,只有在全体 Promise 都为 fulfilled 的情况下,新的实例才会变成 fulfilled 状态。;如果参数中 Promise 有一个失败(rejected),此实例回调失败(rejecte),失败原因的是第一个失败 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
31
32
33
34
35
Promise.all([
new Promise(resolve => {
setTimeout(resolve, 1000, "p1");
}),
new Promise(resolve => {
setTimeout(resolve, 2000, "p2");
}),
new Promise(resolve => {
setTimeout(resolve, 3000, "p3");
})
])
.then(result => {
console.info("then", result);
})
.catch(error => {
console.info("catch", error);
});
// [p1,p2,p3]

Promise.all([
new Promise(resolve => {
setTimeout(resolve, 1000, "p1");
}),
new Promise(resolve => {
setTimeout(resolve, 2000, "p2");
}),
Promise.reject("p3 error")
])
.then(result => {
console.info("then", result);
})
.catch(error => {
console.info("catch", error);
});
// p3 error

获取 cnode 社区的 精华贴的前十条内容

1
2
3
4
5
6
7
8
9
10
11
12
fetch("https://cnodejs.org/api/v1/topics?tab=good&limit=10")
.then(res => res.json())
.then(res => {
const fetchList = res.data.map(item => {
return fetch(`https://cnodejs.org/api/v1/topic/${item.id}`)
.then(res => res.json())
.then(res => res.data);
});
Promise.all(fetchList).then(list => {
console.log(list);
});
});

Promise.race() 竞态执行

Promise.race 也会将多个 Promise 实例封装成一个新的Promise实例,只不过新的 Promise 的状态取决于最先改变状态的 Promise 实例的状态。

在前端最典型的一个用法是为 fetch api 模拟请求超时。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Promise.race([
fetch("https://cnodejs.org/api/v1/topics?tab=good&limit=10").then(res =>
res.json()
),
new Promise((resolve, reject) => {
setTimeout(reject, 1, "error");
})
])
.then(result => {
console.info("then", result);
})
.catch(error => {
console.info("catch", error); // 进入这里
});

上述例子中只要请求 未在 1 毫秒内结束就会进入 .catch() 方法中,虽然不能将请求取消,但是超时模拟却成功了

Promise.resolve(value) && Promise.reject(reason)

这两个方法都能用来创建并返回一个新的 Promise , 区别是 Promise.resolve(value) 携带进新的 Promise 状态是 fulfilled。而 Promise.reject(reason) 带来的 rejected

有的时候可以用来简化一些创建 Promise 的操作如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const sleep = (time = 0) => new Promise(resolve => setTimeout(resolve, time));
// 这里创建一个 睡眠,并且打印的链
Promise.resolve()
.then(() => {
console.log(1);
})
.then(() => sleep(1000))
.then(() => {
console.log(2);
})
.then(() => sleep(2000))
.then(() => {
console.log(3);
});

有时也用来 手动改变 Promise 链中的返回状态 ,当然这样实际上和 直接返回一个值,或者是 使用 throw Error 来构造一个错误,并无区别。到底要怎么用 就看个人喜好了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
new Promise((resolve, reject) => {
setTimeout(() => {
resolve("resolve"); // 1.
}, 1000);
})
.then(result => {
return Promise.reject("reject1"); // 2.
})
.then(
result => {
return Promise.resolve(result + "2");
},
err => {
return Promise.resolve(err); // 3.
}
)
.then(res => {
console.log(res); // 4.
})
.catch(err => {
console.log(err + "err");
});
// reject1

几个例子

下面来看几个例子:

关于执行顺序,具体可搜索,js 循环

1
2
3
4
5
6
7
8
9
10
new Promise((resolve, reject) => {
console.log("step 1");
resolve();
console.log("step 2");
}).then(() => {
console.log("step 3");
});
console.log("step 4");

// step 1, step 2, step 4 , step 3

在使用 Promise 构造函数构造 一个 Promise 时,回调函数中的内容就会立即执行,而 Promise.then 中的函数是异步执行的。

关于状态不可变更

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let start;
const p = new Promise((resolve, reject) => {
setTimeout(() => {
start = Date.now();
console.log("once");
resolve("success");
}, 1000);
});
p.then(res => {
console.log(res, Date.now() - start);
});
p.then(res => {
console.log(res, Date.now() - start);
});
p.then(res => {
console.log(res, Date.now() - start);
});

Promise 构造函数只执行一次,内部状态一旦改变,有了一个值,后续不论调用多少次then()都只拿到那么一个结果。

关于好像状态可以变更

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("success");
}, 1000);
});

const p2 = p1.then((resolve, reject) => {
throw new Error("error");
});

console.log("p1", p1);
console.log("p2", p2);

setTimeout(() => {
console.log("p1", p1);
console.log("p2", p2);
}, 2000);

观察这一次的打印
第一次打印出两个 Promise 的时候都是 pending ,因为 p2 是基于 p1 的结果,p1 正在 pending ,立即打印出的时候肯定是 pending ;第二次打印的时候,因为 p1 的状态为 resolved ,p2 为 rejected ,这个并不是已经为 fulfilled 状态改变为 rejected ,而是 p2 是一个新的 Promise 实例,then() 返回新的 Promise 实例。

关于透传

1
2
3
4
5
6
7
8
Promise.resolve(11)
.then(1)
.then(2)
.then(3)
.then(res => {
console.info("res", res);
});
// 11

给 then 方法传递了一个非函数的值,等同于 then(null),会导致穿透的效果,就是直接过掉了这个 then() ,直到符合规范的 then() 为止。

Promise 的串行调用

使用 Array.reduce 方法串行执行 Promise

1
2
3
4
5
6
7
8
const sleep = (time = 0) => new Promise(resolve => setTimeout(resolve, time));
[1000, 2000, 3000, 4000].reduce((Promise, item, index) => {
return Promise.then(res => {
console.log(index + 1);
return sleep(item);
});
}, Promise.resolve());
// 在分别的等待时间后输出 1,2,3,4

这篇文章到这里就基本上结束了,相信 如果能理解上面的内容,并且在实际项目中使用的话。应该会让工作更高效吧,对于新的异步使用应该也会更加的得心应手。Promise 的使用相对简单,可能后续再出一篇如何实现一个 Promise 吧

那些收集的 Promise 的优质文章。

🔲 ☆

从零学习 canvas (一)

由于上一篇描述的原因。有图像处理的需求,于是我就开始学习 canvas 啦,和以前的一样,这一篇也是一边学一边写,敲出来的。有不正确的地方,欢迎指出。
canvas 本身的 api 描述是比较简单,但是衍生出来的东西,操作,图像处理,动画,性能,还是非常的多的。所以对于 canvas 的学习不出意外的话,将会是一个系列。这就是第一篇了。下面就开始吧

前言

1
<canvas id="canvas" style="background:blue;">浏览器不支持canvas</canvas>

在不支持 canvas 的浏览器中,显示标签中的内容。
绘图区域 默认是 300 x 150。
canvas 中的宽高是实际的宽高,css 中的宽高会等比缩放。
在开始绘图之前需要先,获取绘图环境。

1
2
3
4
5
const canvas = document.querySelector('#canvas');
if(canvas.getContext){
const context = canvas.getContext('2d');
// .... 绘制
}

API

绘制方块

  • fillRect(x, y, width, height):绘制矩形,默认黑色
  • strokeRect(x, y, width, height):带边框的矩形,默认黑色,默认 1px 。但是显示出来可能有区别
  • clearRect(x, y, width, height)
    清除指定矩形区域,让清除部分完全透明。
    1
    2
    3
    4
    5
    6
    {
    // 边框实际上被加粗了
    context.strokeRect(100,100,50,50);
    // 正常边框 1px
    context.strokeRect(160.5,160.5,50,50);
    }

设置绘图样式

  • fillStyle = color: 填充颜色(绘制 canvas 是有顺序的)
  • lineWidth = value: 线宽度,是一个数值
  • strokeStyle = color:边线颜色
1
2
3
4
5
6
7
{
context.strokeStyle='rgba(0,0,255,0.5)';
context.lineWidth=5;
// 调整 fillRect/ strokeRect 的顺序将有不一样的表现
context.strokeRect(160.5,160.5,50,50);
context.fillRect(160.5,160.5,50,50);
}

边界绘制

  • lineJoin = type:边界连接点样式
    • miter/默认;round/圆角;bevel/斜角
  • lineCaP = type:端点样式
    • butt/默认;round/圆角;square/高度多出未为宽一半的值

绘制路径

  • beginPath():开始绘制路径
  • closePath():结束绘制路径(,不是必需的)
  • moveTo(x,y):移动到绘制的点,坐标x以及y
  • lineTo(x,y):绘制一条从当前位置到指定x以及y位置的直线。
  • fill(): 填充
  • stroke(): 边框
1
2
3
4
5
6
7
8
9
10
11
{
context.beginPath();
context.moveTo(100,100);
context.lineTo(150,100);
context.lineTo(100,150);
context.closePath();
// 填充
context.fill();
// 边框
context.stroke();
}

绘制弧

  • arc(x, y, radius, startAngle, endAngle, anticlockwise):绘制圆
    • x,y 起始坐标点,radius 半径大小。
    • startAngle ,endAngle。 圆弧的起始与结束,x轴方向开始计算,单位以弧度表示。弧度 = 角度 * Math.PI/180
    • anticlockwise 可选的Boolean值 ,如果为 true,逆时针绘制圆弧,反之,顺时针绘制。
  • 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为结束点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
context.moveTo(200,200);
context.arc(200,200,150,0,90 * Math.PI / 180, true);
// context.closePath();
context.stroke();

context.moveTo(100,150);
context.arcTo(100,100,200,100,50);
context.stroke();

// 贝塞尔曲线
context.beginPath();
context.moveTo(75,25);
context.quadraticCurveTo(25,25,25,62.5);
context.quadraticCurveTo(25,100,50,100);
context.quadraticCurveTo(50,120,30,125);
context.quadraticCurveTo(60,120,65,100);
context.quadraticCurveTo(125,100,125,62.5);
context.quadraticCurveTo(125,25,75,25);
context.stroke();
}

状态的保存与恢复

  • save():保存路径
  • restore():恢复路径

变换

  • translate(x, y):偏移。x 是左右偏移量,y 是上下偏移量,如右图所示。
  • rotate(angle): 旋转。旋转的角度(angle),它是顺时针方向的,以弧度为单位的值。
  • scale(x, y) :缩放。x,y 分别是横轴和纵轴的缩放因子,它们都必须是正值。值比 1.0 小表示缩小,比 1.0 大则表示放大,值为 1.0 时什么效果都没有。
  • transform(m11, m12, m21, m22, dx, dy)
1
2
3
4
context.translate(200,100);
context.rotate(Math.PI / 180 * 30);
context.scale(1.6,1);
context.fillRect(10,10,50,50);

实例

画板

画板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
(function(){
document.body.innerHTML='<canvas id="canvas" width="1000" height="1000">浏览器不支持</canvas>';
var canvas = document.querySelector('#canvas');
if(!canvas.getContext) return;
var context = canvas.getContext('2d');
function move(ev){
const left = ev.clientX - canvas.offsetLeft;
const top = ev.clientY - canvas.offsetTop;
context.lineTo(left,top);
context.stroke();
}
canvas.addEventListener('mousedown',(ev)=>{
const left = ev.clientX - canvas.offsetLeft;
const top = ev.clientY - canvas.offsetTop;
context.moveTo(left,top);
document.addEventListener('mousemove',move);
})
document.addEventListener('mouseup',()=>{
document.removeEventListener('mousemove',move);
})
})();

旋转的小方块

旋转的小方块

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
(function(){
document.body.innerHTML='<canvas id="canvas" width="1000" height="1000">浏览器不支持</canvas>';
var canvas = document.querySelector('#canvas');
if(!canvas.getContext) return;
var context = canvas.getContext('2d');
let num = 0;
let num2 = 0;
let value = 1;
// context.moveTo(200,200);
context.translate(200,200);
function start(){
num++;
context.save();
context.fillStyle="#fff";
context.fillRect(-200, -200, canvas.width, canvas.height);
if(num2===100){
value = -1;
}else if(num2===0){
value = 1;
}
num2 += value;
context.scale(num2 / 50,num2 / 50);
context.rotate(num * Math.PI / 180);
context.translate(-50,-50);
context.fillStyle="#000";
context.fillRect(0,0,100,100);
context.restore();
requestAnimationFrame(start);
}
start();
})();

时钟

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
(function() {
document.body.innerHTML='<canvas id="canvas" width="1000" height="1000">浏览器不支持</canvas>'
const canvas = document.querySelector('#canvas');
if (!canvas.getContext) return;
let context = canvas.getContext('2d');
function time() {
context.clearRect(0, 0, canvas.width, canvas.height);
let x = 200;
let y = 200;
let r = 150;
// 绘制秒的刻度
context.lineWidth = 1;
context.beginPath();
Array(60).fill(0).forEach((item, index) => {
context.moveTo(x, y);
context.arc(x,y,r,6 * index * Math.PI / 180,6 * (index + 1) * Math.PI / 180,false);
});
context.closePath();
context.stroke();
// 清空中间的部分
context.fillStyle = '#fff';
context.beginPath();
context.moveTo(x, y);
context.arc(x, y, r * 20 / 21, 0, 360 * Math.PI / 180, false);
context.closePath();
context.fill();

// 绘制分钟刻度
context.lineWidth = 2;
context.beginPath();
Array(12).fill(0).forEach((item, index) => {
context.moveTo(x, y);
context.arc(x,y,r,30 * index * Math.PI / 180,30 * (index + 1) * Math.PI / 180,false);
});
context.closePath();
context.stroke();

context.fillStyle = '#fff';
context.beginPath();
context.moveTo(x, y);
context.arc(x, y, r * 19 / 21, 0, 360 * Math.PI / 180, false);
context.closePath();
context.fill();

// 计算时针,分针,秒针的旋转角度
var date = new Date();
var hour = date.getHours();
var minute = date.getMinutes();
var second = date.getSeconds();
var hourValue = (-90 + hour * 30 + minute / 2 + second / 60) * Math.PI / 180;
var minuteValue = (-90 + minute * 6 + second / 12) * Math.PI / 180;
var secondValue = (-90 + second * 6) * Math.PI / 180;

// 小时
context.lineWidth = 4;
context.beginPath();
context.moveTo(x, y);
context.arc(x, y, r * 8 / 21, hourValue, hourValue, false);
context.closePath();
context.stroke();
// 分
context.lineWidth = 2;
context.beginPath();
context.moveTo(x, y);
context.arc(x, y, r * 15 / 21, minuteValue, minuteValue, false);
context.closePath();
context.stroke();
// 秒
context.beginPath();
context.moveTo(x, y);
context.arc(x, y, r * 18 / 21, secondValue, secondValue, false);
context.closePath();
context.stroke();
// 重新开始
setTimeout(time, 1000);
}
time();
})();
🔲 ⭐

浅谈 electron 中的 session 管理(隔离)

已经有很长一段时间没有产出博客了。
一. 是因为花了很多时间去专研业务,能够做到目前的基本业务流程理清,大致了然于胸(导致了一个问题:有人找我解决问题,我可能会先问一句,你的需求是什么?)
二. 确实是自己这一段时间确实懈怠了,每天上班回去就不想敲代码了,看看剧,看看小说,刷刷微博。虽然在组内有过一些分享,整理过一些东西,但是却没有将其在博客产出了。
这样的情况,让我明显的感觉到自己的成长速度相对于第一年成长的速度,慢了几个等级。这让我有一种危机感,于是克服这种懈怠,跳出舒适区,继续强健自己。重新回归吧。
最后。还是引用这个博客的描述:”兴趣遍地都是,专注和坚持才是真正稀缺的。”

不多说了,开始吧。

基础介绍

由于公司的项目内部调整,有幸接触 2 个星期的 electron 开发。(然后我又被拥抱变化了。。)实现了一个多账号的切换,并且同时对于多账号的聊天窗口做一个浏览器 tab 的集成的需求,这里对于接触到的知识点,做一个总结。以免完全忘记(忽略代码规范,我自己都看不下去)

electron-中文文档

在我加入项目之前,壳就已经搭好了,我只是在之上去开发。然而我接触时间太短,然后就撤离了,只能说一些我看到的和用到的部分。( 其它存在的问题, 比如:安全,目前没有更多的精力去解决)

了解之后,最开始的项目搭建是使用的 electron-quick-start 来快速的构建出 一个 electron 客户端的项目。

由于项目需要快速迭代和试错。也没有使用大多数客户端项目将所有资源存在本地,然后再去更新本地资源的形式,而是在客户端暴露 sdk 的情况下 直接 load 了一个 远程地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const electron = require('electron')
const {BrowserWindow} = electron
const path = require('path')
let mainWindow = new BrowserWindow({
width: 1280,
height: 768,
icon: path.resolve(__dirname ,'./build/icon.png'),
title:'客户端',
webPreferences: {
webSecurity: false,
allowRunningInsecureContent: true,
preload: path.resolve(path.join(__dirname, './common/sdk.js'))
}
})
mainWindow.loadURL('https://baidu.com',{extraHeaders: 'pragma: no-cache\n'})

SDK 部分,实际上也没有做太多的封装,直接就暴露出来了。
大概是下面这些

1
2
3
4
5
6
{
const storage = require('electron-json-storage'); // 缓存
const notifier = require('node-notifier') // 通知功能
const charset = require('superagent-charset');
const request = charset(require('superagent')); // HTTP
}

关于此项目 electron 暴露出来的内容,我能聊的大致就是这些了。至于图标,打包成可安装文件,客户端快捷键的设置,并没有太认真的去看,不过应该在网上多搜索,是能找到答案的。 electron-github-issues 实际上能解决绝大多数的问题。

session 模块

关于 electron session 模块,就和文档中的描述一致,session 模块可以用来创建一个新的 Session 对象,然后 有 session.fromPartition(partition) 进行自定义的设置。
你也可以通过使用 webContents 的属性 session 来使用一个已有页面的 session (webContents 是 BrowserWindow 的属性.)

在经过实际的测试发现,在主进程之外无法直接使用electron.session 来获取到 session 对象:{ defaultSession: [Getter], fromPartition: [Function] }

所以在最后,我只能是通过 webContents 中的 session 来处理。

当然就算是这样,也有很多解决方案,但是我目前使用了我认为最简单的一个。直接修改 本地所有的 cookies。

在 BrowserWindow 中

在文档中发现 可以直接在用 BrowserWindow 是可以直接通过 webPreferences 参数来对于 session 进行最初的设置的。

  • webPreferences 参数是个对象,它的属性:
    • session Session - 设置界面 session. 而不是直接忽略 session 对象 , 也可用 partition 来代替, 它接受一个 partition 字符串. 当同时使用 session 和 partition , session 优先级更高. 默认使用默认 session .
    • partition String - 通过 session 的 partition 字符串来设置界面 session. 如果 partition 以 persist: 开头, 这个界面将会为所有界面使用相同的 partition. 如果没有 persist: 前缀, 界面使用历史 session. 通过分享同一个 partition, 所有界面使用相同的 session. 默认使用默认 session.

在 webview 中

1
2
<webview src="https://github.com" partition="persist:github"></webview>
<webview src="http://electron.atom.io" partition="electron"></webview>

在 webview 中同样支持 partition 的设置。规则同上。

但是除此之外 webview 也同样提供了一个方法 <webview>.getWebContents()去获取 到 webview 所属的 webContents。 这样的话,我们也可以直接使用它 session 的属性进行处理

店铺切换

登陆窗口

首先我们需要去创建出一个登陆窗口去让用户把账号给添加到目前的登陆流程中来。然后通过回调函数,将一个必要信息传到主窗口做登陆完成的处理(或者是使用 ipcMain EventEmitter 形式,最终只是需要拿到值。)

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
{
/**
* 创建一个 登录 的窗口。
* 用于 session 隔离
* Promise 中有 {partition,userinfo,cookies}
* @returns Promise
*/
function createLoginWin(partition){
partition = partition || `persist:${Math.random()}`;
const charset = require('superagent-charset');
const request = charset(require('superagent')); // HTTP
let BrowserWindow = new require('electron').remote.BrowserWindow;
let presWindow = new BrowserWindow({
width: 1280,
height: 768,
title:'用户登陆',
webPreferences: {
webSecurity: false,
allowRunningInsecureContent: true,
partition
}
});
let webContents = presWindow.webContents;
return new Promise(function(resove,reject){
// webContents.openDevTools();
presWindow.loadURL('http://taobao.com/#/login');
webContents.on("did-navigate-in-page", function() {
// 这里可以看情况进行参数的传递,获取制定的 cookies
webContents.session.cookies.get({},function(err,cookies){
if(err){
presWindow.close(); // 关闭登陆窗口
return reject(err);
}
// 这一步并不是必需的。
request
.get('http://taobao.com/userinfo')
.query({ _: Date.now() }) // query string
.set("Cookie", cookies.map(item=>`${item.name}=${item.value};`).join(' '))
.end(function(err,res){
presWindow.close();
if(err) {return reject(err);}
if(!res || !res.body || !res.body.result !== 1){
return reject(res.body)
}
let obj = { partition,cookies,userinfo:res.body.data}
resove(obj);
})
})
});
})
}
}

至于信息的存储的话,是使用了 electron-json-storage 将用户的值存储到本地。这里可以随意。

切换用户

上面只是创建了新用户登录的窗口。那么对于旧有的(目前登录)用户信息,做一个初始化同步存储下来的操作。(保持结构一致,(除了 partition 不存在之外))为了后续的 使用方便,可以封装几个对于当前窗口 cookies 操作的函数

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
const cookies =  {
getCurrCookies(params={}){
let currWin = require('electron').remote.getCurrentWindow();
let currSession = currWin.webContents.session;
return new Promise((resove,reject)=>{
currSession.cookies.get(Object.assign({},params),function(err,cookies){
if(err){return reject(err);}
resove(cookies);
})
})
},
removeCurrCookies(cookies = []){
let currWin = require('electron').remote.getCurrentWindow();
let currSession = currWin.webContents.session;
let err = [];
let apiCount = 0;
return new Promise((resove,reject)=>{
cookies.forEach(item=>{
currSession.cookies.remove(`http://${item.domain}`,item.name ,function(err){
if(err){return err.push(err);}
apiCount = apiCount + 1;
})
if(err.length === apiCount){
resove({message:'cookie 清除成功'});
}else{
reject(err);
}
})
})
},
setCurrCookies(cookies = []){
let currWin = require('electron').remote.getCurrentWindow();
let currSession = currWin.webContents.session;
let err = [];
let apiCount = 0;
return new Promise((resove,reject)=>{
cookies.forEach(item=>{
currSession.cookies.set(Object.assign({},item,{
url:`http://${item.domain}`,
name:item.name
}),function(err){
if(err){
return err.push(err)
}
apiCount = apiCount + 1;
})
if(err.length === apiCount){
resove({message:'cookie 设置成功!'});
}else{
reject(err);
}
})
})
}
}

有了这几个函数。结合我们上面,将用户登录信息保存下来的部分,切换店铺就变得异常简单了。

流程如下:

获取当前 –> 清除当前 –> 获取目标 –> 设置当前 –> 重新载入

多 webview 聊天窗口

先来上一个截图。

在我的使用中,直接将聊天窗口创建出来,一个新的 BrowserWindow ,html 中会创建多个 webview

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function openChatTool(data=[]){
// 需要打开的聊天窗口集合,里面会有我们在上面存下来的信息
let random = Math.random();
let BrowserWindow = new require('electron').remote.BrowserWindow;
let presWindow = new BrowserWindow({
width: 1280,
height: 768,
title:'聊天窗口',
webPreferences: {
webSecurity: false,
allowRunningInsecureContent: true,
}
});
// presWindow.webContents.openDevTools();
presWindow.loadURL(`http://${location.host}/chat.html?v=${Math.random()}`);
presWindow.webContents.on('did-finish-load', function() {
// 使用了 send 方法在线程中进行信息传递,在 chat.html 中 可以使用 ipcRenderer接受 如:electron.ipcRenderer.on('chatList',()=>{})
presWindow.webContents.send('chatList', data);
});
}

chat.html 中 tab 切换的部分在此直接略过

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
const electron = require('electron');
electron.ipcRenderer.on('chatList', function(event, data) {
console.log(data);
const webview = document.createElement('webview');
webview.allowpopups = true;
webview.disablewebsecurity = true;
webview.className = index == 0 ? 'active' : '';
// 直接使用 之前存下来的 partition,是最简单的形式
// 当然也可以不使用这个,在下面的事件中 session.cookies.set 将 cookie 设置进去
webview.partition = item.partition || `persist:${Math.random()}`;
webview.src="http://chat.com";
document.body.appendChild(webview);
webview.addEventListener('did-finish-load',function(event){
let webviewContents = webview.getWebContents();
if(webview.getURL()=='http://chat.com/index'){
webviewContents.webContents.session.cookies.get({},function(err,cookies){
// 处理登录失效。重新登录的逻辑。还需要结合别的事件来处理
// 这里可以直接拿到 webview 内的 session 信息
// 代码略
// 可以在外部插入代码
webview.executeJavaScript(`console.log(11)`,()=>{ console.log('insert dom success')})
})
}
})
})

这样需求就搞定了,但是实际上我用到的只是非常少的一部分,并且完成的也不算好。 单单是一个 session 模块中的东西我也还有很多没有去详细的尝试和理解的。不过这个需求整体下来,感觉 electron 还是非常有趣的。只不过接触的时间还太短,没有挖掘出更多有好玩的东西,要是之后有了时间,可以考虑用他写一个自己的应用吧。

🔲 ☆

HTML5常用标签分类

文章是很早之前的笔记,做了一些属性上的补充,现发布到博客中来

十年踪迹的博客:HTML5 元素选择流程图

HTML5基本介绍

HTML5 设计思想

  • 兼容已有内容
  • 避免不必要的复杂性
  • 解决现实的问题
  • 优雅降级
  • 尊重事实标准
  • 用户->开发者->浏览器厂商->标准制定者->理论完美

语法

  • 标签不区分大小写,推荐小写
  • 空标签可以不闭合,比如 input / meta
  • 属性不必引号,推荐双引号
  • 某些属性值可以省略,比如 required,readonly

HTML5常用标签分类

一. HTML文档标签

  1. <!DOCTYPE>: 定义文档类型.
  2. <html>: 定义HTML文档.
  3. <head>: 定义文档的头部.(头部内包含)
  4. <meta>: 定义元素可提供有关页面的元信息,比如针对搜索引擎和更新频度的描述和关键词(由于规范没有规定关于 mete 中各种属性的强制定义,所以不同的浏览器都都可以通过 mete 来声明一些规则)
    • 参考:移动端的头部标签
    • <meta charset ="UtF-8">
    • <meta name ="keywords" conten="关键词">
    • <meta name ="description" conten="页面介绍">
    • <meta name =" viewport" conten="initial-scale=1">
  5. <base>:定义页面上的所有链接规定默认地址或默认目标.
  6. <title>: 定义文档的标题.
  7. <link>: 定义文档与外部资源的关系.
  8. <style>:定义 HTML 文档样式信息.
  9. <body>: 定义文档的主体.(脚本在非必须情况时在主体内容最后)
  10. <script>: 定义客户端脚本,比如 JavaScript.
  11. <noscript>:定义在脚本未被执行时的替代内容.(文本)

二. 布局标签&语义化

  1. <div>:定义块级元素.
  2. <span>:定义行业元素.
  3. <header>5[^footnote]:定义区段或页面的页眉.(头部)
  4. <footer>:定义区段或页面的页脚.(足部)
  5. <section>:定义文档中的区段.
  6. <article>:定义文章.(在<article>中也可以进行内容划分)
  7. <aside>:定义页面内容之外的内容.
  8. <details>:定义元素的细节.
  9. <summary>:定义 <details> 元素可见的标题.
  10. <dialog>:定义对话框或窗口.
  11. <nav>:定义导航.
  12. <hgroup>:定义标题组

三. 表格标签

  1. <table>:定义表格.

    • border=1定义边框
  2. <caption>:定义标题.(规范:必须是 table 的第一个元素)

  3. <thead>:定义页眉.

  4. <tbody>:定义主体.

  5. <tfoot>:定义页脚.

  6. <th>:定义表头.

  7. <tr>:定义一行.

  8. <td>:定义单元格.

    • rowspan="2"跨行(竖直)
    • colspan="2"跨列(水平)
  9. <colgroup><col class="" span="2"></colgroup>:列组,批量的给列做处理

    四. 表单标签

  10. <form>:定义表单.(表单包含在form标签中)

    • novalidate:禁用原生的验证规则
    • 表单提交最好是绑定 submit 事件
  11. <input>:定义输入域.

    • name="username":原生表单提交用于传输的 key 例:key1=value1&key2=value2
    • placeholder="2-10位":描述文字
    • minlength="2"最少(记录一下,一般这些还是走 js)
    • maxlength="10":最多
    • required:是否必填
    • pattern="1\d{10}":正则表达式
    • type="text":input 类型如 search/number/email等都是输入
    • readeonly
    • disabled
  12. <textarea>:定义文本域.(多行)

  13. <label>:定义一个控制的标签.(input 元素的标注)

    • for="abc", abc 为一个 id=”abc”的标签
    • 如果直接把 input 整个包在 label 中也可以有 for 的效果
  14. <fieldset>:定义域.

  15. <legend>:定义域的标题.

  16. <select>:定义一个选择列表.

    • name="aaaa":原生表单提交用于传输的 key
    • size="3":只展示几个
    • multiple:是否开启多选
  17. <optgroup>:定义选择组.

  18. <option>:定义下拉 列表的选项.

  19. <button>:定义按钮.

    • type="submit":默认的是 submit
    • type="button":大部分时间都会手动设置
    • type="reset":重置
  20. <fieldset>:定义围绕表单中元素的边框.

  21. <legend>:定义 fieldset 元素的标题.

  22. <fieldset>:定义选项列表.与input 元素配合使用该元素,来定义 input 可能的值.

  23. <keygen>:定义表单的密钥对生成器字段.

  24. <output>:定义不同类型的输出,比如脚本的输出.

五. 列表标签

列表相关的标签,需要注意其嵌套规则

  1. <ul>:定义无序列表.
  2. <ol>:定义有序列表.
    • 属性 start="1" 表示开始位置
  3. <li>:定义列表项.
  4. <dl>:定义自定义列表.
  5. <dt>:定义自定义列表项.
  6. <dd>:定义自定义的描述.

六. 图像&链接标签

  1. <img>:定义图像.
    • alt="替代文字":必须加!
    • height="200" width="300",可用 css 指定
    • 不指定宽高:原图大小显示
    • 指定宽度:按比例缩放到指定宽度
    • 指定高度:按比较缩放到指定高度
    • 指定宽高:强制按指定宽高显示
  2. <a>:定义超链接.
    • href="url"在这里链接有多种形式
    • 省略协议:href="//baidu.com",自动根据当前页面协议补充
    • 省略协议和host(同时支持相对与绝对路径):href="/index.html"
    • 页面内链接(锚点):href="#test"会找到页面中id 或者name 为test 的元素
    • 链接目标:target="_self"(当前窗口)_blank(新窗口) abc(开一个自定义窗口名称)
  3. <map>:定义图像映射。
  4. <area>:定义图像地图内部的区域.
  5. <figure>:定义媒介内容的分组.(图表,图片,一段代码等)描述<img>内容等。
  6. <figcaption>:定义 <figure> 元素的标题.

七. 音频/视频

  1. <audio>:定义声音内容.
  2. <source>:定义媒介源.
  3. <track>:定义用在媒体播放器中的文本轨道.
  4. <video>:定义视频.

八. 框架标签

  1. <iframe>:内联框架.

九.格式标签

1. 文章标签

  1. <h1>-<h6>:定义 HTML 标题.

  2. <p>:定义段落.

  3. <br>:定义换行.

  4. <hr>:定义水平线.

  5. <bdo>:定义文字方向.

  6. <pre>:定义预格式文本.保留换行等格式

  7. <abbr>:定义缩写.

  8. <address>:定义文档作者或拥有者的联系信息.

  9. <ins>:定义被插入文本.(比如博客中时效性的语句)

  10. <del>:定义被删除文本.

  11. <time>:定义日期/时间.

  12. <wbr>:定义虚拟的空格换行(例如长段的url)

    2. 短语元素标签

  13. <dfn>:定义定义项目.

  14. <code>:定义代码(长短都可)

  15. <samp>:定义计算机代码样本.

  16. <kbd>:定义键盘文本.

  17. <var>:定义文本的变量部分.

  18. <sup>:定义上标文本.

  19. <sub>:定义下标文本.

  20. <cite>:定义引用.(标题/章节/书名)等

  21. <blockguote>:定义长的引用.

    • 属性:cite="url" 表示引用来源
  22. <q>:定义短的引用.(一句话等)

3. 字体样式标签

  1. <em>:定义强调文本.(从一句话中突出某个词语)
  2. <strong>:定义语气更为强烈的强调文本.(重要性,严重性和紧急性)
  3. <i>:显示斜体文本效果.(换一种语调去说已句话时,比如其他语言翻译,对话中的旁白)
  4. <b>:呈现粗体文本效果.(将词语从视觉上和其他部分区分,比如一篇论文摘要中的关键词)
  5. <big>:呈现大号字体效果.
  6. <small>:呈现小号字体效果.
  7. <mark>:定义有记号的文本.(和用户当前行为相关的突出,比如在搜索中匹配到的次,或者一部分内容需要在后面引用时)

十. 其它

  1. <canvas>:定义图形容器,必须使用脚本来绘制图形。
  2. <meter>:定义预定义范围内的度量.
  3. <progress>:定义任何类型的任务的进度.

十一. 一些 HTML全局属性

  • accesskey:键盘快捷键
  • id
  • class
  • style
  • title
  • hidden:标签隐藏
  • lang:语言类型:’en’,’zh-CN’
  • dir:文本排列方向
  • tabindex
  • contenteditable:内容编辑
  • spellcheck:拼写检查
❌