普通视图

发现新文章,点击刷新页面。
昨天以前三省吾身丶丶

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

作者 guowenfh
2019年9月17日 21:36

需求:我们现在有一个获取验证码的按钮,需要在点击后禁用,并且在按钮上显示倒计时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 组件时带来更多的新思路。

参考链接:

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

作者 guowenfh
2019年4月28日 12:00

有一个 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 的异步编程进化历

作者 guowenfh
2018年9月3日 14:32

回调地狱

很久没有进行过创作了,也感觉到了自己的不足。这一篇文章是对于 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 版本。

中文播客推荐

作者 guowenfh
2018年6月16日 15:20

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

黑水公园

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

推荐:

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

从零学习 canvas (一)

作者 guowenfh
2017年10月24日 23:10

由于上一篇描述的原因。有图像处理的需求,于是我就开始学习 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 管理(隔离)

作者 guowenfh
2017年10月21日 22:51

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

不多说了,开始吧。

基础介绍

由于公司的项目内部调整,有幸接触 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常用标签分类

作者 guowenfh
2017年4月21日 23:45

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

十年踪迹的博客: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:拼写检查
❌
❌