ZB-024-01-File与IO

计算机常识

现在典型的计算机里分三级

  • CPU
    • 计算机的大脑,负责一切运算
  • 内存 memory
    • 存数据,断电丢失
  • 硬盘 存数据,容量大,断电不丢失
    1. HDD 以前的硬盘
    2. SSD 现在的硬盘

假设你有台 3GHz的电脑

  • 3*10亿
  • 每秒运行 30亿条指令 如i = i + 1
  • 一个指令大概 0.3ns 这是非常快的

CPU说:这个世界太慢了

来源:cizixs(51CTO博客作者)

链接:http://blog.51cto.com/13188467/2065321

经常听到有人说磁盘很慢、网络很卡,这都是站在人类的感知维度去表述的,比如拷贝一个文件到硬盘需要几分钟到几十分钟,够我去吃个饭啦;而从网络下载一部电影,有时候需要几个小时,我都可以睡一觉了。

最为我们熟知的关于计算机不同组件速度差异的图表,是下面这种金字塔形式:越往上速度越快,容量越小,而价格越高。这张图只是给了我们一个直观地感觉,并没有对各个速度和性能做出量化的说明和解释。而实际上,不同层级之间的差异要比这张图大的多。这篇文章就让你站在 CPU 的角度看这个世界,说说到底它们有多慢。

希望你看到看完这篇文章能明白两件事情:

  • 磁盘和网络真的很慢
  • 性能优化是个复杂的系统性的活

注:所有的数据都是来自这个地址。所有的数据会因为机器配置不同,或者硬件的更新而有出入,但是不影响我们直觉的感受。如果对这些数据比较感兴趣,这个网址给出了不同年份一些指标的数值。

数据

先来看看 CPU 的速度,就拿我的电脑来说,主频是 2.6G,也就是说每秒可以执行 2.6*10^9 个指令每个指令只需要 0.38ns(现在很多个人计算机的主频要比这个高,配置比较高的能达到 3.0G+)。我们把这个时间当做基本单位 1s,因为 1s 大概是人类能感知的最小时间单位。

一级缓存读取时间为 0.5ns,换算成人类时间大约是 1.3s,大约一次或者两次心跳的时间。这里能看出缓存的重要性,因为它的速度可以赶上 CPU,程序本身的 locality 特性加上指令层级上的优化,cache 访问的命中率很高,这最终能极大提高效率。

分支预测错误需要耗时 5ns,换算成人类时间大约是 13s,这个就有点久了,所以你会看到很多文章分析如何优化代码来降低分支预测的几率,比如这个得分非常高的 stackoverflow 问题。

二级缓存时间就比较久了,大约在 7ns,换算成人类时间大约是 18.2s,可以看到的是如果一级缓存没有命中,然后去二级缓存读取数据,时间差了一个数量级。

小知识:为什么需要多层的 CPU 缓存呢?这篇文章通过一个通俗易懂的例子给出了讲解。

我们继续,互斥锁的加锁和解锁时间需要 25ns,换算成人类时间大约是 65s,首次达到了一分钟。并发编程中,我们经常听说锁是一个很耗时的东西,因为在微波炉里加热一个东西需要一分钟的话,你要在那傻傻地等蛮久了。

然后就到了内存,每次内存寻址需要 100ns,换算成人类时间是 260s,也就是4分多钟,如果读一些不需要太多思考的文章,这么久能读完2-3千字(这个快阅读的时代,很少人在手机上能静心多这么字了)。看起来还不算坏,不多要从内存中读取一段数据需要的时间会更多。到了内存之后,时间就变了一个量级,CPU 和内存之间的速度瓶颈被称为冯诺依曼瓶颈。

一次 CPU 上下文切换(系统调用)需要大约 1500ns,也就是 1.5us(这个数字参考了这篇文章,采用的是单核 CPU 线程平均时间),换算成人类时间大约是 65分钟,嗯,也就是一个小时。我们也知道上下文切换是很耗时的行为,毕竟每次浪费一个小时,也很让人有罪恶感的。上下文切换更恐怖的事情在于,这段时间里 CPU 没有做任何有用的计算,只是切换了两个不同进程的寄存器和内存状态;而且这个过程还破坏了缓存,让后续的计算更加耗时。

在 1Gbps 的网络上传输 2K 的数据需要 20us,换算成人类时间是 14.4小时,这么久都能把《星球大战》六部曲看完了(甚至还加上吃饭撒尿的时间)!可以看到网络上非常少数据传输对于 CPU 来说,已经很漫长。而且这里的时间还是理论最大值,实际过程还要更慢一些。

SSD 随机读取耗时为 150us,换算成人类时间大约是 4.5天。换句话说,SSD 读点数据,CPU 都能休假,报团参加周边游了。虽然我们知道 SSD 要比机械硬盘快很多,但是这个速度对于 CPU 来说也是像乌龟一样。I/O 设备 从硬盘开始速度开始变得漫长,这个时候我们就想起内存的好处了。尽量减少 IO 设备的读写,把最常用的数据放到内存中作为缓存是所有程序的通识。像 memcached 和 redis 这样的高速缓存系统近几年的异军突起,就是解决了这里的问题。

从内存中读取 1MB 的连续数据,耗时大约为 250us,换算成人类时间是 7.5天,这次假期升级到国庆七天国外游了。

同一个数据中心网络上跑一个来回需要 0.5ms,换算成人类时间大约是 15天,也就是半个月的时间。如果你的程序有段代码需要和数据中心的其他服务器交互,在这段时间里 CPU 都已经狂做了半个月的运算。减少不同服务组件的网络请求,是性能优化的一大课题。

从 SSD 读取 1MB 的顺序数据,大约需要 1ms,换算成人类时间是 1个月。也就是说 SSD 读一个普通的文件,如果要等你做完,CPU 一个月时间就荒废了。尽管如此,SSD 已经很快啦,不信你看下面机械磁盘的表现。

磁盘寻址时间为 10ms,换算成人类时间是 10个月,刚好够人类创造一个新的生命了。如果 CPU 需要让磁盘泡杯咖啡,在它眼里,磁盘去生了个孩子,回来告诉它你让我泡的咖啡好了。机械硬盘使用 RPM(Revolutions Per Minute/每分钟转速) 来评估磁盘的性能:RPM 越大,平均寻址时间更短,磁盘性能越好。寻址只是把磁头移动到正确的磁道上,然后才能读取指定扇区的内容。换句话说,寻址虽然很浪费时间,但其实它并没有办任何的正事(读取磁盘内容)。

从磁盘读取 1MB 连续数据需要 20ms,换算成人类时间是 20个月。IO 设备是计算机系统的瓶颈,希望读到这里你能更深切地理解这句话!如果还不理解,不妨想想你在网上买的东西,快递送了将近两年,你的心情是怎么样的。

从世界上不同城市网络上走一个来回,平均需要 150ms(参考世界各地 ping 报文的时间),换算成人类时间是 12.5年。不难理解,所有的程序和架构都会尽量避免不同城市甚至是跨国家的网络访问,CDN 就是这个问题的一个解决方案:让用户和最接近自己的服务器交互,从而减少网络上报文的传输时间。

虚拟机重启一次大约要 4s 时间,换算成人类的时间是 3百多年。对于此,我想到了乔布斯要死命优化 Mac 系统开机启动时间的故事。如果机器能少重启而且每次启动能快一点,不仅能救人命,也能救 CPU 的命。

物理服务器重启一次需要 5min,换算成人类时间是 2万5千年,快赶上人类的文明史了。5 分钟人类都要等一会了,更别提 CPU 了,所以没事不要乱重启服务器啊,分分钟终结一个文明的节奏。

文件的本质

一切文件的本质

  • 一段字节流
    • 文本文件(txt/代码/HTML等)
    • 二进制文件
  • 由每个程序负责解释文件的字节流
    • 看到.txt就文本编辑器
    • 看到.png就显示为图片
    • 看到.mp4就播放
    • 文本编辑器打开二进制文件乱码,而JVM/IDEA 认识

为什么文件不用十进制而用十六进制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
十进制 112 

十六进制 70

如果是16进制 秘诀就是 8421
7|0
----------
8421|8421
0111|0000 换算成二进制

这个 8421就是 BCD编码

而十进制 112 换算二进制就没这么快了

所以这就是为什么用16进制来展示,并通过把中间部分拆开两半换算的原因

输入输出IO流详解

  • InputStream
1
2
3
4
5
6
7
8
package java.io;

public abstract class InputStream implements Closeable {
...
// 最重要的方法 read
public abstract int read() throws IOException;
...
}
  • OutputStream
1
2
3
4
5
6
7
8
package java.io;

public abstract class InputStream implements Closeable, Flushable{
...
// 最重要的方法 write
public abstract void write(int b) throws IOException;
...
}

如果你对操作系统不是十分熟悉,请使用绝对路径

  • 从文件读取字节流
1
2
3
4
5
6
7
8
9
10
11
12
13
public static void main(String[] args) throws IOException {
InputStream inputStream = new FileInputStream("绝对路径的文件");
while(true){
int b = inputStream.read();
if(b == -1){
break;
}
System.out.print((char) b);
}

OutputStream outputStream = new FileOutputStream("绝对路径的文件");
outputStream.write('p');
}
  • 从网络读取字节流
1
2
3
4
5
6
HttpClient client = HttpClients.createDefault();
HttpGet get = new HttpGet("https://www.baidu.com");
HttpResponse response = client.execute(get);

System.out.println(response.getStatusLine());
System.out.println((char) response.getEntity().getContent().read());
  • 从其他各种地方读取内容

在java中 fork子进程

1
2
3
ProcessBuilder pb = new ProcessBuilder("ls");
Process process = pb.start();
System.out.println((char)process.getInputStream().read());

java中的File

  • 不要误会!不要误会!不要误会!
    • File并不代表⼀个“⽂件”,它只代表⼀个 路径
    • 抽象的“⽂件”路径:⽂件或者⽂件夹
  • File的常⻅⽅法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 不要背这些api 最重要的是找到如何使用这些
    File file = new File("路径");
    file.isDirectory();
    file.exists();
    file.isFile();
    file.getAbsolutePath();
    file.listFiles(); // List<File>
    file.list();// 文件名
    file.isAbsolute(); //是不是绝对路径
    file.getName(); //获取文件名
    file.getParentFile();
    file.getCanonicalPath(); //获取经典路径 如 "./././.././" => /某个目录
  • 绝对路径与相对路径

    • 绝对路径在 windows上 c:/ 或 d:/开头的 ,在linux上 以/开头的
    • 相对路径"." 当前路径
  • 读/写⽂件

ZB-023-http

HTTP

HTTP彩蛋

451什么意思

  • 来源于一本小说《华式451度》

代表由于法律原因被禁止的请求

另一个彩蛋 http staus 咖啡壶

  • 没有任何卵用

参考知识

HTTP实战

ReactWheels11-01scroll组件

让内容滚动出现滚动条

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
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
<style>
#wrapper {
height: 50vh;
border:10px solid red;
overflow:auto;
}

.aaa{
background:pink;
height: 90px;
}
</style>
</head>
<body>
<div id="wrapper">
<p class="aaa"></p>
<p class="aaa"></p>
<p class="aaa"></p>
<p class="aaa"></p>
<p class="aaa"></p>
<p class="aaa"></p>
<p class="aaa"></p>
<p class="aaa"></p>
<p class="aaa"></p>
<p class="aaa"></p>
</div>
</body>
</html>

两个div外层 overflow:hidden 内层定位

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
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
<style>
.magic{
height: 30vh;
border:10px solid blue;
position:relative;
/* 隐藏超出的内容 ,这样就看不见滚动条了*/
/*overflow: hidden;*/
}
.wrapper {
border:1px solid red;
overflow:auto;
position: absolute;
left:0;
bottom:0;
/* 让滚动条在超出 magic*/
right:-14px;
top:0;
}


.aaa{
background:pink;
height: 90px;
}
</style>
</head>
<body>
<!--魔法层 包裹这个内容 ,

-->
<h1>wrapper绝对定位四个角占满, right用负值 </h1>
<div class="magic">
<div class="wrapper">
<p class="aaa"></p>
<p class="aaa"></p>
<p class="aaa"></p>
<p class="aaa"></p>
<p class="aaa"></p>
<p class="aaa"></p>
<p class="aaa"></p>
<p class="aaa"></p>
<p class="aaa"></p>
<p class="aaa"></p>
</div>
</div>

<h2>magic 区域设置 overflow: hidden; 就看不见滚动条了</h2>
<div class="magic" style="overflow: hidden;">
<div class="wrapper">
<p class="aaa"></p>
<p class="aaa"></p>
<p class="aaa"></p>
<p class="aaa"></p>
<p class="aaa"></p>
<p class="aaa"></p>
<p class="aaa"></p>
<p class="aaa"></p>
<p class="aaa"></p>
<p class="aaa"></p>
</div>
</div>
<h2>right:-14px为啥?(量出来的!!!) 如果你想做的好 就针对不同系统,去量这个宽度</h2>
</body>
</html>

更多思路参考 simplebar

ReactWheels11-04form组件

超难bug解决

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
errors = {
username:['e1','e2'],
password:['e3','e4']
}

// 错误可能是异步的于是,=》 Promise | string
errors = {
username:['e1',p1,'e2',p2],
password:['e3',p3,'e4',p4]
}

// 利用 Promise.reject('e1')
// 将错误简化,都归类于 Promise
errors = {
username:[p1,p2],
password:[p3,p4]
}

// 我们的目标是
// 把异步的错误
errors = {
username:[p1,p2],
password:[p3,p4]
}

// 异步要等所有的结果, 所以只能使用 Promise.all([])
// 但是 Promise.all 接受的是一个数组
// 从 [p1,p2] 转化为 ['e1','e2']
errors = {
username:['e1','e2'],
password:['e3','e4']
}
  • step01流程
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
errors = {
u:[p1,p2],
p:[p3,p4]
}

// (关联起来)变成
[
[
['u',p1],['u',p2]
],
[
['p',p3],['p',p4]
]
]

// 拍平
[['u',p1],['u',p2],['p',p3],['p',p4]]

// 挨个遍历 ,信息整合
arr = [
Promise ['u',p1],
Promise ['u',p2],
Promise ['p',p3],
Promise ['p',p4]
]
最后调用 Promise.all(arr)


每个结果都是 Promise ['u',p1]
errors = {
username:['e1','e2'],
password:['e3','e4']
}

ReactWheels11-03form组件

proposal optional chaining 提案

1
2
3
4
5
6
var a = {c:[0,2]}

a.c && a.c.join(',')

// 如果有这个提案就可以这样
a?.c.join(',')

异步的表单验证

如用户名是否存在

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const rules = [
{key: 'username',validator: {
name:'unique',
validate(username:string){
axios.get('/check_name',{params:{username}})
.then(()=>{

},()=>{

})
// 但是结果是异步的
// 此时只能用 Promise
}
}
}
]

Promise

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const usernames = ['frank','jack','tom'];
const checkUserName = (username: string,success:()=>void,fail:()=>void) =>{
setTimeout(()=>{
if(usernames.indexOf(username)>0){
success()
}else{
fail();
}
},3000);
}

...


{key: 'username',validator: {
name:'unique',
validate(username:string){
return new Promise((resolve,reject)=>{
checkUserName(username, resolve, reject);
})
}

}
}

Promise

1
2
3
4
5
cosnt x = ()=>{};

Promise.all(promistList).then(x,x);
// 等价于
Promise.all(promistList).finally(x);

Todo

  • 支持子字段编辑(子表单)
  • 支持更多的 type 如 图片上传 / 自定义 type
  • 支持手机端

ReactWheels11-02form组件

Input组件

input.tsx

  • Props 继承 InputHTMLAttributes
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import * as React from "react";
import {InputHTMLAttributes} from "react";
import classes from "../helpers/classes";
import './input.scss';

interface Props extends InputHTMLAttributes<HTMLInputElement>{

}

const Input: React.FunctionComponent<Props> = (props) => {
const {className , ...rest} = props
return (
<input className={classes('fui-input', className)} type="text" {...rest}/>
)
}

export default Input;

input.scss

  • 实现 类似 ant-design 的 border过渡效果
  • 不要上来就写 width/height 绝对有bug
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@import "../helper";
.fui-input{
border:1px solid #ddd;
line-height: 22px;
padding:4px 8px;
border-radius: 4px;
box-shadow: 0 0 0 2px fade_out($main-color,1);
transition: box-shadow 1s;
&:focus{
outline: none;
border-color: $main-color;
box-shadow: 0 0 0 2px fade_out($main-color,0.5);
}
}

如何让form表单不写样式就对齐呢?

table布局
table布局
table布局

它虽然渲染速度慢,但是它会自动计算宽度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<table>
<tr>
<td>用户名</td>
<td>aaa</td>
</tr>
<tr>
<td>用户名xxxxxxxxx</td>
<td>aaa</td>
</tr>
</table>

// table 会把整个列宽都算一遍
// 找到最大宽度的 td 以它的宽度去渲染td 对应 column
// 所以才会慢

只有中国人才需要的字段对齐

实现任意长度的两端字对齐

1
2
3
4
用  户  名
密 码

// 这个只有中国人才会这样

table 的td

  • 不接受 margin
  • 接受 padding

button

button.tsx

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
import * as React from "react";
import {ButtonHTMLAttributes} from "react";
import classes from "../helpers/classes";
import './button.scss';

interface Props extends ButtonHTMLAttributes<HTMLButtonElement>{
level?: 'important' | 'danger' | 'normal'
}

const Button: React.FunctionComponent<Props> = (props) => {
const {className ,children,level, ...rest} = props
return (
<button
className={classes('fui-button',`fui-button-${level}`,className)}
{...rest}>
{children}
</button>
)
}

Button.defaultProps = {
level: 'normal'
}

export default Button;

button.scss

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
@import "../helper";

.fui-button{
padding:4px 16px;
line-height: 22px;
transition: all 250ms;
cursor:pointer;
&:focus{
outline: none;
}

&-normal{
color: #333;
border:1px solid lighten(#333,50%);
background: white;
&:hover , &:focus{
border-color: $main-color;
color:$main-color;
}
}
&-important{
color: white;
border:1px solid $main-color;
background: $main-color;
&:hover , &:focus{
background: lighten($main-color,10%);
border-color: lighten($main-color,10%);
}
}
}

ReactWheels11-01form组件

Form 组件

form基本思路

form.example.tsx

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
import * as React from "react";
import Form from "./form";
import {useState, Fragment} from "react";

const FormExample:React.FunctionComponent = ()=>{
const [formData] = useState({
username:'',
password:''
})
const [fields] = useState([
{name:'username',label:'用户名',input:{type:'text'}},
{name:'password',label:'密码',input:{type:'password'}},
])
return (
<div>
<Form value={formData} fields={fields}
buttons={
<Fragment>
<button>提交</button>
<button>返回</button>
</Fragment>
}
/>
</div>
)
}

export default FormExample;

form.tsx

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
import * as React from "react";
import {ReactFragment} from "react";

interface Props {
value: { [K: string]: any };
fields: Array<{name:string,label:string,input:{type:string}}>;
buttons: ReactFragment;
}

const Form:React.FunctionComponent<Props> = (props)=>{
// key 是为了消除警告
return (
<div>
{props.fields.map( f =>
<div key={f.name}>
{f.label}
<input type={f.input.type}/>
</div>
)}
<div>
{props.buttons}
</div>
</div>
)
}

export default Form;

受控组件和非受控组件

1
2
3
4
5
6
const FormExample:React.FunctionComponent = ()=>{
const [name,setName] = useState('hjx')
return (
<input value={name} />
)
}

页面显示一个 input 然后 value 为 “hjx”

  • 但是无法编辑
  • 而且有警告
1
2
3
4
5
6
7
8
9
10
11
12
checkPropTypes.js:20 Warning: Failed prop type: You provided a `value` prop to a form field without an `onChange` handler. This will render a read-only field. If the field should be mutable use `defaultValue`. Otherwise, set either `onChange` or `readOnly`.
in input (created by FormExample)
in FormExample (created by Context.Consumer)
in Route
in div (created by Content)
in Content
in div (created by Layout)
in Layout
in div (created by Layout)
in Layout
in Router (created by HashRouter)
in HashRouter

想改怎么办?

  • 添加 onChange 事件
1
2
3
4
5
6
const FormExample:React.FunctionComponent = ()=>{
const [name,setName] = useState('hjx')
return (
<input value={name} onChange={(e)=> setName(e.target.value)} />
)
}

困惑

为什么以前什么都不加就可以写,而现在在React要这样写

关系到React的设计哲学

  • UI = f(state) 只要 状态 state 没变 UI就不该变
    1
    2
    3
    fn(1) //返回 2
    fn(1) //返回 2
    // 再次调用时,应该还是返回2 ,这样才符合函数式

其他方式

1
2
3
4
5
6
7
8
9
10
11
12
13
const FormExample:React.FunctionComponent = ()=>{
const [name,setName] = useState('hjx')
return (
<div>
<input value={name} onChange={(e)=> setName(e.target.value)} />
<input defaultValue={name} type="text"/>
</div>
)
}

第一个可以修改 但要添加 onChange

第二个也可以修改 但是拿不到 value的值此时你只能使用 Ref

Ref

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const FormExample:React.FunctionComponent = ()=>{
const [name,setName] = useState('hjx')
// 泛型告诉 refInput.current 就是 input元素
const refInput = useRef<HTMLInputElement>(null);
const x = ()=>{
// refInput.current 就是 input
// 断言有值
console.log(refInput.current!.value)
}
return (
<div>
<input value={name} onChange={(e)=> setName(e.target.value)} />

<input defaultValue={name} ref={refInput} type="text" onBlur={x}/>
</div>
)
}
  • 第一种叫做受控组件(React推荐使用)
  • 第二种叫做 非受控组件(它只是比较方便,如只是显示一个值)

onChange输出最新的值

form.example.tsx

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
import * as React from "react";
import Form, {FormValue} from "./form";
import {useState, Fragment} from "react";


const FormExample:React.FunctionComponent = ()=>{
const [formData,setFormData] = useState<FormValue>({
username:'aaa',
password:'111'
})
const [fields] = useState([
{name:'username',label:'用户名',input:{type:'text'}},
{name:'password',label:'密码',input:{type:'password'}},
])

const onSubmit = (e:React.FormEvent<HTMLFormElement>)=>{
console.log(formData)
}

return (
<div>
<Form
value={formData} fields={fields}
buttons={
<Fragment>
<button>提交</button>
<button>返回</button>
</Fragment>
}
onChange={(newValue) => setFormData(newValue)}
onSubmit={onSubmit}
/>
</div>
)
}

export default FormExample;

form.tsx

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 * as React from "react";
import {ReactFragment} from "react";

// FormValue 就是把整个 value传递回去 有性能问题 但是你有10000条数据的表单吗? 最多提交20条
export interface FormValue{
[K: string]: any
}

interface Props {
value: FormValue;
fields: Array<{name:string,label:string,input:{type:string}}>;
buttons: ReactFragment;
onSubmit: React.FormEventHandler<HTMLFormElement>;
onChange: (value: FormValue)=> void
}

const Form:React.FunctionComponent<Props> = (props)=>{
const formData = props.value;
const onSubmit: React.FormEventHandler<HTMLFormElement> = (e)=>{
e.preventDefault();
props.onSubmit(e);
}
const onInputChange = (name:string,value:string)=>{
const newFormValue = {...formData, [name]:value};
props.onChange(newFormValue)
console.log(name,value)
}
// key 是为了消除警告
return (
<form onSubmit={onSubmit}>
{props.fields.map( f =>
<div key={f.name}>
{f.label}
<input type={f.input.type} value={formData[f.name]}
onChange={(e) => onInputChange(f.name, e.target.value)}
//还可以用bind onChange={onInputChange.bind(null,f.name)}
/>
</div>
)}
<div>
{props.buttons}
</div>
</form>
)
}

export default Form;

表单验证思路

Validator.tsx

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
import {FormValue} from "./form";

interface FormRule {
key: string;
required?: boolean;
minLength?: number;
maxLength?: number;
pattern?: RegExp
}

type FormRules = Array<FormRule>

interface FormErrors {
[K: string]: string[]
}

function isEmpty(value: any){
if(value === undefined || value === null || value === ''){
return true;
}
return false;
}

const Validator = (formValue: FormValue, rules: FormRules): FormErrors =>{
let errors: any = {};
const addError = (key: string, message: string) => {
if(errors[key] === undefined){
errors[key] = [];
}
errors[key].push(message);
}
rules.map(rule => {
console.log(rule);
const value = formValue[rule.key];
if (rule.required && isEmpty(value)) {
addError(rule.key, '必填');
}
if (rule.minLength && !isEmpty(value) && value.length < rule.minLength) {
addError(rule.key, '太短');
}
if (rule.maxLength && !isEmpty(value) && value.length > rule.maxLength) {
addError(rule.key, '太长');
}
if( rule.pattern ){
if(!(rule.pattern.test(value))){
addError(rule.key,'格式不正确')
}
}
})
return errors;
}

export default Validator;

form.example.tsx

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
import * as React from "react";
import Form, {FormValue} from "./form";
import {useState, Fragment} from "react";
import Validator from "./validator";


const FormExample:React.FunctionComponent = ()=>{
const [formData,setFormData] = useState<FormValue>({
username:'aaa',
password:'111'
})
const [fields] = useState([
{name:'username',label:'用户名',input:{type:'text'}},
{name:'password',label:'密码',input:{type:'password'}},
])

const onSubmit = (e:React.FormEvent<HTMLFormElement>)=>{
console.log(formData)
const rules = [
{key: 'username',required: true },
{key: 'username',minLength: 3 , maxLength:16 },
{key: 'username',pattern: /^[A-Za-z0-9]+$/},
]
const errors = Validator(formData,rules);
console.log(errors)
}

return (
<div>
<Form
value={formData} fields={fields}
buttons={
<Fragment>
<button>提交</button>
<button>返回</button>
</Fragment>
}
onChange={(newValue) => setFormData(newValue)}
onSubmit={onSubmit}
/>
</div>
)
}

export default FormExample;

UNIX 编程艺术

  • 有些代码,没有明显的BUG
  • 另外一些代码,明显没有BUG (我们应该写这种)

ReactWheels10-制作官网

1
2
3
4
5
6
7
8
9
10
11
12
yarn add -D file-loader@3.0.1

webpack 文件添加配置

{
test: /\.(png|jpe?g|gif)$/i,
use: [
{
loader: 'file-loader',
},
],
},

example.tsx 里 引入图片

1
2
3
const logo = require('./logo.png');

<img src={logo} alt=""/>

IDEA高级技巧

  • 清空缓存,重新载入项目
1
顶部菜单=> file => invalidate Caches / Restart

如何展示源代码?

  • !!raw-loader 代表开启加载
  • !!raw-loader!路径 代表加载的文件
1
2
3
4
5
yarn add raw-loader@2.0.0

然后在 example.tsx里
const x = require('!!raw-loader!./lib/icon/icon.example.tsx');
console.log(x.default)

demo.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import * as React from 'react';

interface Props {
code:string;
}

const Demo:React.FunctionComponent<Props> = (props) => {
return (
<div>
{props.children}
<pre>
{props.code}
</pre>
</div>
)
}

export default Demo;

icon.demo.tsx

1
2
3
4
5
6
7
8
9
10
11
12
import React from "react";
import IconExample from "./icon.example";
import Demo from "../../demo";

const IconDemo = () => {
return (
<Demo code={require('!!raw-loader!./icon.example.tsx').default}>
<IconExample/>
</Demo>
)
}
export default IconDemo;
  • 不要企图传递组件路径让 demo.tsx 里去动态加载,因为不支持,试过了
    1
    const code = require(`!!raw-loader!${prop.path}`).default

源代码高亮

1
yarn add prism-react-renderer@0.1.6 -D

demo.tsx

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
import * as React from 'react';
import Highlight, {defaultProps} from "prism-react-renderer";
import {useState} from "react";

interface Props {
code:string;
}

/*
const Demo:React.FunctionComponent<Props> = (props) => {
return (
<div>
{props.children}
<pre>
{props.code}
</pre>
</div>
)
}
*/

const Demo:React.FunctionComponent<Props> = (props) => {
const [codeVisible,setCodeVisible] = useState(false);
const code = (
<Highlight {...defaultProps} code={props.code} language="jsx">
{({className, style, tokens, getLineProps, getTokenProps}) => (
<pre className={className} style={style}>
{tokens.map((line, i) => (
<div {...getLineProps({line, key: i})}>
{line.map((token, key) => (
<span {...getTokenProps({token, key})} />
))}
</div>
))}
</pre>
)}
</Highlight>
);
return (
<div className="example">
{props.children}
<div>
<button onClick={()=>setCodeVisible(!codeVisible)}>查看代码</button>
</div>
<div>
{codeVisible && code}
</div>

</div>
)
}

export default Demo;

部署

修改 webpack.config.example.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const base = require('./webpack.config')
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = Object.assign({},base,{
mode:'production',
output: {
path: path.resolve(__dirname, 'doc'),
},
entry:{
example:'./example.tsx'
},
plugins:[
new HtmlWebpackPlugin({
template:'example.html',
filename: "index.html"
})
]
})

新建分支

1
2
3
4
5
6
7
8
9
10
11
git branch gh-pages
git checkout gh-pages
# 提交到远程
git push orgin gh-pages:gh-pages
# 删除所有文件
rm -rf *
# 提交
git add .
git commit -m "remove all files"

# 回到master

doc.sh

1
2
3
4
5
6
7
8
9
#!/bin/env bash

yarn doc
git checkout gh-pages
mv -f doc/* ./
git add .
git commit -m "update"
git push
git checkout -

ReactWheels09-layout组件

参考 ant-design的 Layout组件

如何给 Layout 传递 style呢?

  • CSSProperties
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<Layout style={{height:500}}></Layout>


import React, { CSSProperties } from 'react';

interface Props {
style: CSSProperties;
}
const Layout: React.FunctionComponent = (props) => {
return (
<div className={sc()}>
...
</div>
)
}
  • 继承 React.HTMLAttributes
1
2
3
4
5
6
7
8
interface Props extends React.HTMLAttributes<HTMLElement>{
}

const Layout: React.FunctionComponent<Props> = (props) => {
return (
...
)
}

解决className 覆盖问题

如果直接组件上...props 导致我们组件的className 被覆盖

1
2
3
4
5
6
7
8
9
const Layout: React.FunctionComponent<Props> = (props) => {
// rest 代表剩余的属性值,这样就不会把我们自己的 className 覆盖了
const {className, ...rest} = props;
return (
<div className={[sc(),className].join(' ')} {...rest}>
{props.children}
</div>
)
}

基本实现

layout.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import React from 'react';
import {scopedClassMaker} from '../classes';
import './layout.scss'

const sc = scopedClassMaker('fui-layout')

interface Props extends React.HTMLAttributes<HTMLElement>{
}

const Layout: React.FunctionComponent<Props> = (props) => {
const {className, ...rest} = props;
return (
<div className={sc('',{extra:className})} {...rest}>
{props.children}
</div>
)
}

export default Layout;

Temp.tsx 内容替换为 [header|footer|content|aside] 就是对应的组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import React from 'react';
import {scopedClassMaker} from '../classes';

const sc = scopedClassMaker('fui-layout')

interface Props extends React.HTMLAttributes<HTMLElement>{
}

const Temp: React.FunctionComponent<Props> = (props) => {
const {className, ...rest} = props;
return (
<div className={sc('temp',{extra:className})} {...rest}>
{props.children}
</div>
)
}

export default Temp;

layout.scss

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@import "../helper";
.fui-layout{
border:1px solid red;
display: flex;
flex-direction: column;
&-content{
flex-grow:1;
border:1px solid green;
}
// layout 里的 layout
& &{
flex-grow: 1;
border:1px solid blue;
flex-direction: row;
}
}

遇到问题 就是 菜单单独在左侧,右侧上中下结构,我们的布局出新问题了

  • 尝试获取 layout 的 children 来改className 的方式。
  • 限制用户传递的 children 必须是元素 而不是字符串
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
interface Props extends React.HTMLAttributes<HTMLElement>{
// 限制我们的组件的 children 不能是一个字符串,必须是一个元素
children: ReactElement | Array<ReactElement>
}

const Layout: React.FunctionComponent<Props> = (props) => {
const {className, ...rest} = props;
// 报错 因为它不一定是数组
// if(props.children.length){}


// 只能通过断言 来绕过检查
// 但是这样就导致 容易写出垃圾代码 ,只是让你写的时候思考 该不该这样写
if((props.children as Array<ReactElement>).length){
// 此时还需要继续断言
(props.children as Array<ReactElement>).map(node => {
console.log(node);
})
}
return (
<div className={sc('',{extra:className})} {...rest}>
{props.children}
</div>
)
}

export default Layout;

通过判断 Layout 里是否有 Aside 来添加 hasAside

layout.tsx

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
import React, { ReactElement } from 'react';
import {scopedClassMaker} from '../classes';
import './layout.scss';
import Aside from './aside';

const sc = scopedClassMaker('fui-layout')

interface Props extends React.HTMLAttributes<HTMLElement>{
// 限制我们的组件的 children 不能是一个字符串,必须是一个元素
children: ReactElement | Array<ReactElement>
}

const Layout: React.FunctionComponent<Props> = (props) => {
const {className, ...rest} = props;
// 报错 因为它不一定是数组
// if(props.children.length){}

// 只能通过断言 来绕过检查
// 但是这样就导致 容易写出垃圾代码 ,只是让你写的时候思考 该不该这样写
let hasAside = false;
if((props.children as Array<ReactElement>).length){
(props.children as Array<ReactElement>).map(node => {
console.log(node);
// 一旦发现有 aside 就附加一个类 hasAside
if(node.type === Aside){
hasAside = true;
}
})
}
return (
<div className={sc('',{extra:[className, hasAside && 'hasAside'].join(' ')})} {...rest}>
{props.children}
</div>
)
}

export default Layout;

layout.scss

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@import "../helper";
.fui-layout{
border:1px solid red;
display: flex;
flex-direction: column;
/* 如果Layout 含有Aside 则它为row 同时它里面的 Layout 还是 column*/
&.hasAside{
flex-direction: row;
.fui-layout{
flex-direction: column;
}
}
&-content{
flex-grow:1;
border:1px solid green;
}
// layout 里的 layout
& &{
flex-grow: 1;
border:1px solid blue;
flex-direction: row;
}
}

消除代码里的 let 因为它违反函数式

  • 我们用一个boolean值 存这个 hasAside
1
2
3
4
5
6
7
8
9
10
11
if((props.children as Array<ReactElement>).length){
const hasAside =(props.children as Array<ReactElement>)
.reduce((result,node) => result || node.type === Aside,false);
}
return (
<div className={sc('',{extra:[className, hasAside && 'hasAside'].join(' ')})} {...rest}>
{props.children}
</div>
)

// 此时的问题是 hasAside 无法在 return 里用

改写完成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
interface Props extends React.HTMLAttributes<HTMLElement>{
// 限制我们的组件的 children 不能是一个字符串,必须是一个元素
children: ReactElement | Array<ReactElement>
}

const Layout: React.FunctionComponent<Props> = (props) => {
const {className, ...rest} = props;

const children = props.children as Array<ReactElement>
const hasAside = children.length &&
children.reduce((result,node) => result || node.type === Aside,false);
return (
<div className={sc('',{extra:[className, hasAside && 'hasAside'].join(' ')})} {...rest}>
{props.children}
</div>
);
}

export default Layout;

Object.entries()

1
2
3
4
5
6
7
Object.entries({a:1,c:2,b:3})
返回
[
["a",1],
["c":2],
["b":3]
]

重构 scopedClassMaker

  • classes.tsx
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
function classes(...names: (string | undefined)[]) {
return names.filter(Boolean).join(' ');
}

export default classes;

interface Options {
extra: string | undefined
}

/*
{
hasAside:true,
clearfix:false,
mt20:true
}
*/
interface ClassToggles {
[K: string]: boolean
}

function scopedClassMaker(prefix: string){
return function (name?: string | ClassToggles,options?: Options){
let name2;
let result;

if( typeof name === 'string' || name === undefined){
name2 = name;
result = [prefix,name2].filter(Boolean).join('-');
}else{
// ['hasAside','x']
name2 = Object.entries(name).filter(kv => kv[1]).map(kv => kv[0]);
result = name2.map( n =>{
return [prefix,n].filter(Boolean).join('-')
}).join(' ')
// ['fui-layout-hasAside', 'fui-layout-x']
}
if(options && options.extra){
return [ result, options && options.extra].filter(Boolean).join(' ');
}else{
return result;
}
}
}

export {scopedClassMaker};

再次重构它——函数式

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
interface ClassToggles {
[K: string]: boolean
}

function scopedClassMaker(prefix: string){

return function (name: string | ClassToggles , options?: Options){
const namesObject = (typeof name === 'string' || name === undefined) ?
{[name]:name} :
name;
const scoped = Object
.entries(namesObject)
.filter(kv => kv[1] !==false)
.map(kv => kv[0])
.map(name => [prefix, name]
.filter(Boolean)
.join('-')
).join(' ');
if(options && options.extra){
return [ scoped, options && options.extra].filter(Boolean).join(' ');
}else{
return scoped;
}
}
}

将函数式进行到底

1
2
3
4
5
6
7
8
9
10
11
const scopedClassMaker = (prefix: string) =>
(name: string | ClassToggles , options?: Options) =>
Object
.entries(name instanceof Object ? name : {[name]:name})
.filter(kv => kv[1] !==false)
.map(kv => kv[0])
.map(name => [prefix, name]
.filter(Boolean)
.join('-'))
.concat( options && options.extra || [])
.join(' ');

测试 scopedClassMaker

1
2
3
4
5
6
7
8
9
10
11
describe('scopedClassMaker', () => {
it('接收字符串 或 对象', () => {
const sc = scopedClassMaker('fui-layout')
expect(sc('')).toEqual('fui-layout')
expect(sc('x')).toEqual('fui-layout-x')
expect(sc({y:true,z:false})).toEqual('fui-layout-y')
expect(sc({y:true,z:true})).toEqual('fui-layout-y fui-layout-z')
expect(sc({y:true,z:true},{extra:'red'})).toEqual('fui-layout-y fui-layout-z red')
})

})

代码连接

react-gulu-test-4

ReactWheels08-dialog组件

Dialog组件

设计API

  • 我们用的是React ,最好的办法就是抄同行,交互方式是用户学习软件的过程
  • 你的dialog 不能和同行有明显的不同,否则用户学起来费劲
  • API 最好一致,否则别人迁移到你的UI也很费劲
  1. 确定UI
  2. API
1
2
3
4
5
6
7
8
9
// 第一种方式 标签
<Dialog visable={x}>
你的内容
</Dialog>

// 第二种方式 js
alert('你好').then(fn)
confirm('确定').then(sure,cancel)
model(<table>...</table>)

初版 dialog

lib/dialog/dialog.example.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import React, { useState } from 'react';
import Dialog from './dialog';

export default function(){
const[x,setX] = useState(false);
return (
<div>
<button onClick={ ()=>setX(!x) }>click</button>
<Dialog visible={x}>
<div>hi</div>
</Dialog>
</div>
)
};

lib/dialog/dialog.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import React from 'react';

interface Props {
visible:boolean;
}

const Dialog:React.FunctionComponent<Props> = (props) => {
return props.visible ?
// 对于 react来说 你必须返回一个节点或者 null
// 直接 {props.children} 报错
<div>{props.children}</div> :
null
}
export default Dialog;

Fragment 使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import React, { Fragment } from 'react';

interface Props {
visible:boolean;
}

const Dialog:React.FunctionComponent<Props> = (props) => {
return props.visible ?
// 遮罩层的div 和 dialog分开,因为点击遮罩层要消失
// <Fragment> 是为了渲染时不多渲染一个 div 和通过编译
// 不能直接返回多个节点必须一个根节点,
// 而是只它里面的内容
<Fragment>
<div className="fui-dialog-mask"></div>
<div className="fui-dialog">
{props.children}
</div>
</Fragment>
:
null
}
export default Dialog;

dialog垂直水平居中

1
2
3
4
position:fixed;
top:50%;
left:50%;
transform: translate(-50%,-50%);

dialog.scss

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
.fui-dialog{
position: fixed;
background: white;
width:10em;
height: 10em;
border-radius: 4px;
top:50%;
left:50%;
transform: translate(-50%,-50%);
&-mask{
position: fixed;
top:0;
left:0;
width:100%;
height: 100%;
background: fade-out(black, 0.5);
}
}

dialog.tsx

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
import React, { Fragment } from 'react';
import './dialog.scss';
import {Icon} from '../index';

interface Props {
visible:boolean;
}

const Dialog:React.FunctionComponent<Props> = (props) => {
return props.visible ?
// 遮罩层的div 和 dialog分开,因为点击遮罩层要消失
// <Fragment> 是为了渲染时不多渲染一个 div 和通过编译
// 不能直接返回多个节点必须一个根节点,
// 而是只它里面的内容

// 关闭按钮不要在 header里 因为这样就必须有 header
<Fragment>
<div className="fui-dialog-mask"></div>
<div className="fui-dialog">
<div className="fui-dialog-close">
<Icon name="close"/>
</div>
<header className="fui-dialog-header">提示</header>
<main className="fui-dialog-main">
{props.children}
</main>
<footer className="fui-dialog-footer">
<button>ok</button>
<button>cancel</button>
</footer>
</div>
</Fragment>
:
null
}
export default Dialog;

此时的问题是 如何简化 className很重复,而且麻烦

  • 偏函数使用
1
2
3
4
5
6
7
8
9
10
11
function scopedClassMaker(prefix: string){
return function x(name?: string){
return [prefix,name].filter(Boolean).join('-');
}
}

const scopedClass = scopedClassMaker('fui-dialog');
const sc = scopedClass;

sc() // fui-dialog
sc('hello') // fui-dialog-hello

给我们的组件加一个样式

  • 我们的组件样式应该有一个全局的控制如字体和盒模型
  • 新建 lib/index.scss
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
我们的组件都是 fui 开头的

[class^=fui-]{
box-sizing: border-box;
}

// 这个选择器的缺陷是无法选取到 伪元素
// 这个选择器的缺陷是无法选取到 伪元素
// 这个选择器的缺陷是无法选取到 伪元素
// 优化后

[class^=fui-]{
box-sizing: border-box;
&::after,
&::before{
box-sizing: border-box;
}
}

svg 为父亲元素的颜色

1
fill:currentColor;

React.cloneElement 的用法

  • dialog 传递的按钮是个数组 buttons
1
2
3
4
5
6
7
8
9
10
11
12
13
// 直接这样会报错 ,意思是你缺少key
<footer>
{props.buttons}
</footer>

// 答案就是使用 map 和 React.cloneElement
<footer>
{props.buttons.map((button,index) =>
React.cloneElement(button,{key:index})
)}
</footer>

// 不过这里有个性能的损失,就是每次都会 map 产生新的 button

能凑乎使用的 dialog

dialog.tsx

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
import React, { Fragment, ReactElement } from 'react';
import './dialog.scss';
import {Icon} from '../index';
import {scopedClassMaker} from '../classes';

interface Props {
visible:boolean;
buttons:Array<ReactElement>;
onClose:React.MouseEventHandler;
closeOnClickMask?:boolean;
}

const scopedClass = scopedClassMaker('fui-dialog');
const sc = scopedClass;

const Dialog:React.FunctionComponent<Props> = (props) => {
const onClickClose:React.MouseEventHandler = (e)=>{
props.onClose(e);
}
const onClickMask:React.MouseEventHandler = (e)=>{
if(props.closeOnClickMask){
props.onClose(e);
}
}
return props.visible ?
<Fragment>
<div className={sc('mask')} onClick={onClickMask}></div>
<div className={sc()}>
<div className={sc('close')} onClick={onClickClose}>
<Icon name="close"/>
</div>
<header className={sc('header')}>提示</header>
<main className={sc('main')}>
{props.children}
</main>
<footer className={sc('footer')}>
{props.buttons.map((button,index) =>
React.cloneElement(button,{key:index})
)}
</footer>
</div>
</Fragment>
:
null
}
Dialog.defaultProps = {
closeOnClickMask : false
}
export default Dialog;

dialog.scss

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 "../helper";
.fui-dialog{
position: fixed;
background: white;
min-width:20em;
border-radius: 4px;
top:50%;
left:50%;
transform: translate(-50%,-50%);
&-mask{
position: fixed;
top:0;
left:0;
width:100%;
height: 100%;
background: fade-out(black, 0.5);
}
&-header{
font-size: 22px;
padding:8px 16px;
border-bottom: 1px solid grey;
}
&-main{
padding:8px 16px;
min-height: 6em;
}
&-footer{
padding:8px 16px;
border-top: 1px solid grey;
display: flex;
justify-content: flex-end;
}
&-close{
position: absolute;
bottom:100%;
left:100%;
background: $main-color;
width: 2em;
height: 2em;
border-radius: 50%;
transform: translate(-50%,50%);
display: flex;
justify-content: center;
align-items: center;
color: white;;
}
}

dialog.example.tsx

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
import React, { useState } from 'react';
import Dialog from './dialog';

export default function(){
const[x,setX] = useState(false);
const[y,setY] = useState(false);
return (
<div>
<div>
<h1>example1</h1>
<button onClick={ ()=>setX(!x) }>click</button>
<Dialog visible={x}
buttons={
[
<button onClick={()=>{setX(false)}}>1</button>,
<button onClick={()=>{setX(false)}}>2</button>
]
}
onClose={()=>{setX(false)}}
>
<div>hi</div>
</Dialog>
</div>

<div>
<h1>example2</h1>
<button onClick={ ()=>setY(!y) }>click</button>
<Dialog visible={y}
closeOnClickMask={true}
buttons={
[
<button onClick={()=>{setY(false)}}>1</button>,
<button onClick={()=>{setY(false)}}>2</button>
]
}
onClose={()=>{setY(false)}}
>
<div>hi</div>
</Dialog>
</div>
</div>
)
};

一个 z-indexBug

  • 场景如下
1
2
3
4
5
6
<div style="z-index=10">
aaa
</div>

<Dialog/>
// 此时我们的 dialog 没有盖住 div
  • 你可能会想给我们的 dialog 设置 z-index:9999
    • 如果是平级那没问题
    • 但如果你的 Dialog 在一个容器里那个容器的 z-index:9 你就算是 z-index:99999 也没用
      1
      2
      3
      4
      5
      6
      7
      <div style="z-index=10">
      aaa
      </div>
      <div style="z-index:9">
      <!-- 就算dialog 是 z-index:99999 也没用,因为父 div这个层级已经被压住了 -->
      <Dialog>
      </div>

解决办法就是:不要出现在任何元素的里面

解决方案是 react portal

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
render() {
// React mounts a new div and renders the children into it
return (
<div>
{this.props.children}
</div>
);
}

// react portal

render() {
// React does *not* create a new div. It renders the children into `domNode`.
// `domNode` is any valid DOM node, regardless of its location in the DOM.
return ReactDOM.createPortal(
this.props.children,
domNode
);
}

推荐dialog的 z-index 设置为1 ,因为用户可以改啊!

z-index 结构划分

1
2
3
4
5
6
7
8
背景区 0~5

内容区 10~19
菜单区 50~59

对话框 100~120

广告区 150~170

为什么推荐我们的 dialog z-index:1呢?

  • 因为层级低容易出现,这样别人用的时候再去覆盖 fui-dialog{z-index:100}

如何不通过标签创造dialog呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
const alert = (content:string) =>{
const component = <Dialog visible={true} onClose={()=>{
ReactDOM.render(React.cloneElement(component,{visable:false}),div);
// 从div上卸载 dialog
ReactDOM.unmountComponentAtNode(div);
// 删除div
div.remove();

}}>{content}</Dialog>;
const div = document.createElement('div');
document.body.append(div);
ReactDOM.render(component,div);
}

ReactElement 和 ReactNode 区别

  • ReactElement 必须是个标签
  • ReactNode 不仅可以是标签还可以是 “字符串”

重构API

原则就是 立刻完成功能后立刻重构

  • 别过一个小时
  • 别过一个星期后重构
  • 立刻!

重构的目的就是

  • 移除重复,三则重构

dialog.tsx

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
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
import React, { Fragment, ReactElement, ReactNode, ReactFragment } from 'react';
import ReactDOM from 'react-dom';
import './dialog.scss';
import {Icon} from '../index';
import {scopedClassMaker} from '../classes';

interface Props {
visible:boolean;
buttons?:Array<ReactElement>;
onClose:React.MouseEventHandler;
closeOnClickMask?:boolean;
}

const scopedClass = scopedClassMaker('fui-dialog');
const sc = scopedClass;




const Dialog:React.FunctionComponent<Props> = (props) => {
const onClickClose:React.MouseEventHandler = (e)=>{
props.onClose(e);
}
const onClickMask:React.MouseEventHandler = (e)=>{
if(props.closeOnClickMask){
props.onClose(e);
}
}
const x = props.visible ?
<Fragment>
<div className={sc('mask')} onClick={onClickMask}></div>
<div className={sc()}>
<div className={sc('close')} onClick={onClickClose}>
<Icon name="close"/>
</div>
<header className={sc('header')}>提示</header>
<main className={sc('main')}>
{props.children}
</main>
{
props.buttons && props.buttons.length > 0 &&
<footer className={sc('footer')}>
{props.buttons && props.buttons.map((button,index) =>
React.cloneElement(button,{key:index})
)}
</footer>
}

</div>
</Fragment>
:
null

/*
直接 {props.buttons} 会报错 因为 buttons 是数组
需要一个key
*/
return (
ReactDOM.createPortal(x,document.body)
);
}
// 设置默认 props
Dialog.defaultProps = {
closeOnClickMask : false
}

const alert = (content:string) =>{
const onClose = () => {
ReactDOM.render(React.cloneElement(component,{visable:false}),div);
ReactDOM.unmountComponentAtNode(div);
div.remove();
}
const component =
<Dialog
visible={true}
onClose={onClose}
buttons={[ <button onClick={onClose}>OK</button>]}
>
{content}
</Dialog>;
const div = document.createElement('div');
document.body.append(div);
ReactDOM.render(component,div);
}


const confirm = (content:string,yes:()=>void,no:()=>void) =>{
const onClose = () => {
ReactDOM.render(React.cloneElement(component,{visable:false}),div);
ReactDOM.unmountComponentAtNode(div);
div.remove();
}
const onYes = () => {
onClose()
yes && yes();
};
const onNo = () => {
onClose()
no && no();
};
const component = (
<Dialog
visible={true}
onClose={onNo}
buttons={[
<button onClick={onYes}>yes</button>,
<button onClick={onNo}>no</button>
]}
>
{content}
</Dialog>);
const div = document.createElement('div');
document.body.append(div);
ReactDOM.render(component,div);
}

const model = (content: ReactNode | ReactFragment) =>{
const onClose = () => {
ReactDOM.render(React.cloneElement(component,{visable:false}),div);
// 从div上卸载 dialog
ReactDOM.unmountComponentAtNode(div);
// 删除div
div.remove();
}
const component = (
<Dialog visible={true}
onClose={onClose}
>
{content}
</Dialog>
);
const div = document.createElement('div');
document.body.append(div);
ReactDOM.render(component,div);

return onClose;
}

export default Dialog;
export {alert, confirm , model };

提取公共

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
const model = (content: ReactNode , buttons?: Array<ReactElement>,afterClose?:() => void) => {
const close = () => {
ReactDOM.render(React.cloneElement(component,{visable:false}),div);
ReactDOM.unmountComponentAtNode(div);
div.remove();
console.log('close')
}
const component =
<Dialog
visible={true}
onClose={()=>{
close();
afterClose && afterClose();
}}
buttons={buttons}
>
{content}
</Dialog>;
const div = document.createElement('div');
document.body.append(div);
ReactDOM.render(component,div);
return close;
}

const alert = (content:string) =>{
const button = <button onClick={() => close()}>OK</button>;
const close = model(content,[button])
}


const confirm = (content:string,yes?:() => void,no?:() => void) =>{
const onYes = () => {
close()
yes && yes();
};
const onNo = () => {
close()
no && no();
};
const buttons = [
<button onClick={onYes}>yes</button>,
<button onClick={onNo}>no</button>
]
const close = model(content,buttons, no);
}

总结

  • scopedClass 高阶函数
  • Fragment 可以不生成多余的节点
  • react portal 传送门

    1
    ReactDOM.createPortal(渲染元素 , document.body)
  • 动态生成组件 参考 alert 实现

  • 闭包传 API 如 close

代码连接

react-gulu-test-4