Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
89c50fc032 | ||
|
|
c81446eff5 | ||
|
|
d5f96ff5a3 | ||
|
|
db3449ace1 | ||
|
|
1dddebbb1f | ||
|
|
77e2da3f2d | ||
|
|
019076e87f | ||
|
|
18e058af5b | ||
|
|
fe7b930b95 | ||
|
|
1a9be6aaf8 | ||
|
|
3110990b90 | ||
|
|
e5e2d49ae1 | ||
|
|
94381de60b | ||
|
|
70f20e6861 | ||
|
|
fab11f31d2 | ||
|
|
8af572d9f9 | ||
|
|
0c542af0ab | ||
|
|
4b3aae675d | ||
|
|
532d3bd6f0 | ||
|
|
cb20d65c2c | ||
|
|
07b6017451 | ||
|
|
2c5f8ca4f3 | ||
|
|
cd543b1fd4 | ||
|
|
88560b6626 | ||
|
|
1cf5e339c9 | ||
|
|
4ac67508f4 | ||
|
|
d2044c4636 | ||
|
|
59629228f9 | ||
|
|
eeb4272d49 | ||
|
|
e8ae8557d2 | ||
|
|
f0ab10ac09 | ||
|
|
b7b0409cd5 | ||
|
|
13963fbdd2 | ||
|
|
9b9d02b9de | ||
|
|
ec65f684bb | ||
|
|
022b1fdbbd | ||
|
|
ff121c7e1b | ||
|
|
a04380c1c1 | ||
|
|
3646b36db5 | ||
|
|
aaaa28953b | ||
|
|
9609101870 | ||
|
|
afb601a303 | ||
|
|
c72477ed7d | ||
|
|
dc09550e0c | ||
|
|
dc66e76dd1 | ||
|
|
cecef87d13 | ||
|
|
d0ca3a348f | ||
|
|
de4e309f39 | ||
|
|
ea56ac10ce | ||
|
|
90660c6d34 | ||
|
|
fcf04083a5 | ||
|
|
a1ea12a736 | ||
|
|
02a795bb8a | ||
|
|
6bffed8eb5 | ||
|
|
ddd5ceb74a | ||
|
|
3c6cee2f49 | ||
|
|
fc2f0c6f68 | ||
|
|
0dcf208315 | ||
|
|
7730834e83 | ||
|
|
91875ab232 | ||
|
|
d81e6d7813 | ||
|
|
3e299d85e9 | ||
|
|
455e1693f4 | ||
|
|
2a17e62107 | ||
|
|
0a090cf0a7 | ||
|
|
c13f0af435 | ||
|
|
8ee7995ddc | ||
|
|
0e8c749e68 | ||
|
|
ee0450ffb2 | ||
|
|
e0d8be6916 | ||
|
|
7c4d37a428 | ||
|
|
eeaebad903 | ||
|
|
c624e7e607 | ||
|
|
0f724a20cf | ||
|
|
c7922c4a0e | ||
|
|
9d98f72fd3 | ||
|
|
db9a256efa | ||
|
|
aedfdd4b91 | ||
|
|
99d5c51d42 | ||
|
|
086b733288 | ||
|
|
6f9ccaef9e | ||
|
|
dba92f52b5 | ||
|
|
61512fba55 | ||
|
|
542faafe04 | ||
|
|
70468eae8a | ||
|
|
9e0c2027d2 | ||
|
|
6ac4e965bb | ||
|
|
5edddb5890 | ||
|
|
706ac612ff | ||
|
|
444a56e333 | ||
|
|
2e966d717d | ||
|
|
237d856cfe | ||
|
|
d12c076774 | ||
|
|
3e3cc53715 | ||
|
|
ea4d30385a | ||
|
|
1b03871be8 | ||
|
|
42e068aab7 | ||
|
|
280efe4545 | ||
|
|
9b54c138a7 | ||
|
|
c253b7d60f | ||
|
|
367bba2692 | ||
|
|
e888c4e22c | ||
|
|
8a3425b5e3 | ||
|
|
9081bde65e | ||
|
|
4f783678da | ||
|
|
299a60a881 | ||
|
|
7e651219b0 | ||
|
|
e1e7bc6f57 | ||
|
|
beed8434ec | ||
|
|
ab13166c0a | ||
|
|
71a48cde3b | ||
|
|
c603ac0d1d | ||
|
|
316d3d98aa | ||
|
|
0465cf43db | ||
|
|
65de19ddb6 | ||
|
|
9c0f11cc55 | ||
|
|
754ceb3d5a | ||
|
|
dcc5247ba0 | ||
|
|
02de198a8b | ||
|
|
b7cfc4c6e3 | ||
|
|
46a094e711 | ||
|
|
b5cd1b2379 | ||
|
|
cfe94e6aa7 | ||
|
|
3362f25775 | ||
|
|
87f9704831 | ||
|
|
6c7084dfe1 | ||
|
|
164590159d | ||
|
|
b8ab46d26b | ||
|
|
183b4fc41a | ||
|
|
2b7ab34978 | ||
|
|
2ca7132569 | ||
|
|
aac9aa34f8 | ||
|
|
ad8a645462 | ||
|
|
2dbe4d6368 | ||
|
|
d76253ef75 | ||
|
|
46b68f1933 | ||
|
|
8aab27bc3f | ||
|
|
22f118e1b7 | ||
|
|
922eb6dc59 | ||
|
|
20526273d3 | ||
|
|
f3222c646b | ||
|
|
9ae28c5e85 | ||
|
|
e72318adb4 | ||
|
|
2682dc0504 | ||
|
|
8b4611b47a | ||
|
|
25938c60a6 | ||
|
|
9920f0dbb7 | ||
|
|
90f7d89db7 | ||
|
|
6ddbdbbbb2 | ||
|
|
3c8c9fd5ef | ||
|
|
91a9bf82fd | ||
|
|
4603ac77d8 | ||
|
|
998940af1b | ||
|
|
24ed724eab | ||
|
|
33b71628ab | ||
|
|
2dbf1429ba | ||
|
|
9867c1ee9d | ||
|
|
e8a0539984 | ||
|
|
685e8ba32a | ||
|
|
6c9493df7b | ||
|
|
1994438792 | ||
|
|
60be3e354b | ||
|
|
9d0bb5c996 | ||
|
|
304dc5fc36 | ||
|
|
819b6d7452 | ||
|
|
b066c8a5d7 | ||
|
|
80fb60c6fb | ||
|
|
6a2031899d | ||
|
|
aedcc2fd56 | ||
|
|
757eb403ac | ||
|
|
87c2745395 | ||
|
|
0d3d8ef54c | ||
|
|
53d5ee1960 | ||
|
|
92a8dfc55c | ||
|
|
c59f46cc1d | ||
|
|
ef3904a2f6 | ||
|
|
994c5844d9 | ||
|
|
3881e51b08 | ||
|
|
815ae1367a | ||
|
|
84ed47dbdf | ||
|
|
252980724a | ||
|
|
225aab476d | ||
|
|
f065539266 | ||
|
|
f8d81fc3f5 | ||
|
|
43e264cc07 | ||
|
|
957737959f | ||
|
|
000e6511e6 | ||
|
|
a652284bbc | ||
|
|
1b88e5ac06 | ||
|
|
79ba9ec6a1 | ||
|
|
a4acf09d8b | ||
|
|
872ef2e346 | ||
|
|
8795ae0aaa | ||
|
|
23c4190d7e | ||
|
|
75d0109102 | ||
|
|
893bb2d97a | ||
|
|
0c2c8c6c03 | ||
|
|
97a313ca1e | ||
|
|
3fd57e26b8 | ||
|
|
aee6f6d6bb | ||
|
|
00fd6e541a | ||
|
|
6cc1af4ca9 | ||
|
|
ed27df0fe2 | ||
|
|
14ebdf19ca | ||
|
|
e89a1b0c7e | ||
|
|
a1bd0ec7a5 | ||
|
|
82d3665606 | ||
|
|
67a289e750 | ||
|
|
1b0a7ed8ea | ||
|
|
21ba3e0640 | ||
|
|
d5159b914e | ||
|
|
c45a3d09d5 | ||
|
|
c3ea6f2b17 | ||
|
|
fb98d1febc | ||
|
|
036a9779eb | ||
|
|
beb89cf31b | ||
|
|
5d65a777bd | ||
|
|
115d108404 | ||
|
|
f573e16473 | ||
|
|
ac53e3a325 | ||
|
|
d3dad3b151 | ||
|
|
7843682b1e | ||
|
|
11c34a9edc | ||
|
|
923c3990f7 | ||
|
|
b091d40925 | ||
|
|
5c76c9d567 | ||
|
|
0ad9a98d33 | ||
|
|
b9fb7c80f2 | ||
|
|
03519c41f6 | ||
|
|
8ff1e9e21a | ||
|
|
93882be318 | ||
|
|
d0dee4bbd7 | ||
|
|
3cc44000eb | ||
|
|
46cdabeb86 | ||
|
|
71258f1b83 | ||
|
|
1d95103bfb | ||
|
|
b9d9292040 | ||
|
|
7ce55795a2 | ||
|
|
2a420c455e | ||
|
|
069cf8f947 | ||
|
|
af86669e18 | ||
|
|
50c4409e29 | ||
|
|
5c44507443 | ||
|
|
ffb736eb6d | ||
|
|
abc677a5f0 | ||
|
|
3c78a1a659 | ||
|
|
507df796b0 | ||
|
|
0f58ce27dc | ||
|
|
51c8f3bb69 | ||
|
|
c4da7262cb | ||
|
|
a513286318 | ||
|
|
509dfe2c97 | ||
|
|
806ee0177a | ||
|
|
fe4afdf89c | ||
|
|
caec16066a | ||
|
|
7f53bde9a3 | ||
|
|
63e2ca4d0d | ||
|
|
9dbd1fb7b9 | ||
|
|
7d8816c2c2 | ||
|
|
44ae420f52 | ||
|
|
683b22d1ae | ||
|
|
4e58379585 | ||
|
|
1342144fef | ||
|
|
a41f452df0 | ||
|
|
0950c4cdaf | ||
|
|
46ad26c1a4 | ||
|
|
6dd26bee54 | ||
|
|
37d6b5f3f0 | ||
|
|
f23e88338f | ||
|
|
3dda46d760 | ||
|
|
cd6a00bf3a | ||
|
|
6aad1da704 | ||
|
|
03a5a0f484 | ||
|
|
0066b890d4 | ||
|
|
e0caf77d0a | ||
|
|
1517e86999 | ||
|
|
c8f86df3f3 | ||
|
|
752d598d27 | ||
|
|
d8f2e0977d | ||
|
|
f4e75f5e21 |
@@ -0,0 +1,2 @@
|
||||
/node_modules
|
||||
/yarn.lock
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"excludeFiles": [],
|
||||
"rules": {
|
||||
"no-long-code": 0,
|
||||
"no-trailing-punctuation": 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- "10"
|
||||
before_install:
|
||||
- npm i -g lint-md-cli
|
||||
script: lint-md ./
|
||||
@@ -1,54 +0,0 @@
|
||||
# 精读《Web fonts: when you need them, when you don’t》
|
||||
本期精读让我们来聊一聊Web Fonts,文章地址:[https://hackernoon.com/web-fonts-when-you-need-them-when-you-dont-a3b4b39fe0ae](https://hackernoon.com/web-fonts-when-you-need-them-when-you-dont-a3b4b39fe0ae)
|
||||
|
||||
## 文章简介
|
||||
文章分析了Web Fonts的优劣具体使用场景。
|
||||
|
||||
## 主要观点
|
||||
- 作者用一张流程图非常言简意赅地概括了文章的上半部分。
|
||||

|
||||
- 当然上半部分作者也讲了很多案例,其中一个很明显的案例就是维基百科利用字体来提升阅读体验,通过文章内的对比,能直观感受到这一点
|
||||
- 文章后半部分着力介绍了怎么解决Web Font的带来的弊端:认识FOUT带来的问题,如何使用现有的前端解决方案来尽可能避免这个问题,以及样式上优雅降级的几个方案。
|
||||
|
||||
## 把文章带入自己的开发环境
|
||||
作为一个中文开发者,在我们的开发技术栈中,Web Fonts绝对是属于使用频率比较低的那一类的。本次精读选择这篇文章,也正是一探这一个不常见的领域。
|
||||
|
||||
对于英文字母,26个字母可以解决大部分的问题,算上大小写和基本符号,一张ASCII码标就可以包含住。让我再扩展一下,到大部分的西文书写系统,几百个字符就能解决多语言显示的问题了。但是对于汉语而言,Web Fonts真的是,想说爱你不容易,因为常用的汉字就有几千个(你想象中国还有《千字文》这种儿童读物……)。字体这东西跟字符数量直接挂钩,是很难通过压缩来获得性能提升的。
|
||||
|
||||
通常的想法就是用多少,取多少,但是这个方法也就只能适用于标题美化等场景。对于一个系统性的前端工程,我们不可能去实现一个动态字符的字体文件(就是统计这个页面上会产生多少个字符,为这个字符集去生成一个字符子集)
|
||||
|
||||
虽然汉字书写系统和西文书写系统天生存在差异,但是把作者在文章中提出来的几个问题再站在中文的角度上再来看一下,也可以得到一个比较客观的答案。以下是我作为一个普通开发者的自问自答:
|
||||
|
||||
1. **字体对你的品牌很关键吗?**(需要特性字体的中文LOGO基本都用PNG和SVG解决了,Web Font不实用。)
|
||||
2. **字体让你的文字阅读起来更容易了吗?**(我平时开发产品没有成片聚集的文字,用无衬线字体就能满足需求。Web Font很好,我选择“微软雅黑”。)(注:泛指那些好用的支持全字集的系统自带字体;成片的文字适用衬线字体,个人认为中文的衬线字体,不同的字体带来的阅读体验还是有明显差别的。)
|
||||
3. **你需要在不同设备上显示一样的字体吗?**(好像还没这么苛求吧……微软雅黑好看,安卓上的Roboto也很不错啊,Roboto这种字体还针对移动设备有优化,何乐而不为。)
|
||||
4. **用了Web Font你会更开心吗?**(在icon中使用iconfont让我们告别了PNG Sprite图,嗯这很开心。至于文字上用Web Font,有好用的系统字体你不用,你这是何苦呢)(作者也说了,可能折腾半天还没系统字体看着舒服,那就是一行font-family的事情)
|
||||
|
||||
不同产品有着不同的场景,多像文章里问问自己会有最合适的答案。
|
||||
|
||||
## 关于FOUT和FOIT
|
||||
文章中大篇幅地在安利你使用Web Font,但是也很直白地指出了Web Font最大的问题,就是这个FOUT——Flash Of Unstyled Text。连作者毫不避讳地说了句:“噢我的老天,这太丑了!”
|
||||
|
||||
具体表现是采用了Web Font的文案会存在闪动,这个的根本原因在于相比于系统字体,Web Font最大的弊端在于它是异步加载的,你没有办法避免下载它所用的时间。文章中举了一个例子,在一个图文为主的页面中,一个542KB的字体文件,在第9秒才加载完成。在那之前只能以系统字体来展示,而在第9秒加载完成的时候,还会出现替换字体的情况,文字会突然跳动。
|
||||
|
||||
比FOUT更为极端的情况的是FOIT——Flash Of Invisible Text。很多浏览器的行为,并不是默认展示系统字体,而是直接隐藏。那么即使在极快的网速下也很难避免存在一个几百毫秒的时间滞后。
|
||||
|
||||
不过好在,有一个font-display的属性,可以在声明@font-face的时候配合使用。对于未加载Web Fonts的时候,auto属性可以选择隐藏也就是会产生FOIT,swap会产生替换也就是会产生FOUT,还有fallback和optional可以控制先FOIT后FOUT来达到折中方案。
|
||||
|
||||
还有一个思路,那就是预加载,对于字体,浏览器还是能够有效缓存的,如果能够做好预加载,还是不会太影响用户体验的。文章中就提到了一个方案,调用link的rel=preload来做预加载。因为通常加载字体是在CSS中的@font-face被读到的时候才去加载的,那么就会出现先加载CSS,后加载字体的情况。如果利用link预加载,那么在CSS中的@font-face被读到前就已经开始加载了,那么字体加载和CSS加载就可以同时加载,提升速度。
|
||||
|
||||
当然JS是万能的,也有一些库在支持这方面功能,例如bramstein/fontfaceobserver这样的。
|
||||
|
||||
愚以为,FO*T这种情况既然无法避免还是要具体情况具体分析的。如果你的用户网速够快,那么隐藏文字会更好,用户无感知;如果网速不确定,而且是文章为主的内容,那么内容至上就应该先用替代字体显示;如果你正在将Web Font应用在图标等东西上,那么我们自然不愿意看到满屏的方框方框,这种时候就选择隐藏吧。
|
||||
|
||||
文章也提了一点,如果你的字体授权很贵,但用户端深受FO*T折磨,那你还费这钱干嘛。
|
||||
|
||||
## 结论
|
||||
如果能解决FO*T的副作用,Web Font怎么舒服怎么用。但是中文字体大,常用西文字体诸如Google字体库又时常被墙,对于中国开发者,Web Font想说爱你不容易。(还是乖乖用微软雅黑吧,逃……
|
||||
|
||||
## 彩蛋
|
||||
文章里有一段很精彩的话,摘抄出来翻译一下:
|
||||
|
||||
> 如果这个世界上有这样一个Sketch或者Photoshop的插件,可以在你每次打开一个文件的时候,延迟十秒才显示出字体;那么世界上就没有那么多多余的字体了。
|
||||
>
|
||||
> (译者注:删光设计师的电脑里的奇怪字体也能达到相同目的,哈哈哈~)
|
||||
@@ -1,52 +0,0 @@
|
||||
# 精读《快速上手构建ARKit应用》
|
||||
|
||||
原文地址: [how-to-make-your-own-arkit-app-in-5-minutes-using-react-native](https://medium.com/@HippoAR/how-to-make-your-own-arkit-app-in-5-minutes-using-react-native-9d7ce109a4c2)
|
||||
|
||||
## 引言
|
||||
ARKit是苹果推出的增强现实套装,而react-native-arkit是基于此的上层封装。对于前端开发而言,这可能是最快上手ARKit的方式了,本周精读让我们来初窥ARKit和React Native ARKit这个库。
|
||||
|
||||
## 概要
|
||||
本次精读我们带来的是一篇《快速上手构建ARKit应用》,原文链接如上。原文标题更加直接,直译的话是“如何在5分钟里利用react native搭建出你自己的ARKit应用”。确实,这篇文章整体也非常明确,以跑起整个ARKit Demo为最直接最主要的目的。
|
||||
|
||||
跑起ARKit,也很简单。硬件上,只要有一台iPhone 6S以上的手机;软件上,只要准备好最新版本的XCode和日常开发要用的Node环境了就好。按照`react-native-arkit`的里面的README就可以跑起来了。这个库不
|
||||
|
||||
## 3 精读
|
||||
在开始精读前,我先抛出我的问题三连:Why AR? Why ARKit? Why React Native ARKit?
|
||||
|
||||
### 3.1 Why AR?
|
||||
在之前的第43期精读评论中,我们探讨了AR对于和前端结合的可能性。总的来说,AR把前端开发不再局限在有限的屏幕空间上,对于可视化等对前端展示空间有强烈需求的细分领域,AR是一个很值得研究的内容。如果对于这一块内容有兴趣,欢迎回看第43期精读评论 [《精读〈增强现实与可视化〉》](https://github.com/dt-fe/weekly/blob/master/43.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%A2%9E%E5%BC%BA%E7%8E%B0%E5%AE%9E%E4%B8%8E%E5%8F%AF%E8%A7%86%E5%8C%96%E3%80%8B.md)。
|
||||
|
||||
### 3.2 Why ARKit?
|
||||
为什么选择 ARKit 入手进行实验?其因有二。第一,相比于 Microsoft HoloLens 的价格,售价只有它三分之一的iPhone X无论是体积重量,还是性价比,抑或是保有量都是大大占优的。噢对,说到保有量,iPhone 6S及以上都支持ARKit。所以说iPhone是我们身边最容易接触到的AR设备是不为过的。第二,ARKit对于硬件的利用能力非一般的前端库可以做到的。大部分的AR前端库可以做到利用陀螺仪来构建一个三维立体空间。但是ARKit更进一步,他利用高频调用摄像头,通过对图像进行识别分析,可以进行空间感知,例如可以识别出一个平面。而这些都是ARKit所提供的,我们只需要调用它的能力就好了。对于开发者而言,ARKit会比一般的AR库更近一步。
|
||||
|
||||
### 3.3 Why React Native ARKit?
|
||||
对于当下的前端开发,所有事情可以分为两种——0. 可以用 JavaScript 写的 1. 其他。至于为什么选择`react-native-arkit`这个库,原因自然也可以理解。相比于用原生的Swift来开发,React Native 的开发方式对于前端而言明显是更加容易上手了。对于尝试新东西,这也未尝不可。
|
||||
|
||||
### 3.4 About Demo
|
||||
相比于原文中从初始化开始的步骤,官方还提供了一个已经配置好的[官方Demo](https://github.com/HippoAR/ReactNativeARKit)。使用这个,如果环境没有问题,的确只需要5分钟就可以跑起来一个ARKit应用了。
|
||||
|
||||

|
||||
|
||||
上面的图片来自原文,可以看到,在`react-native-arkit`这个库里面的所支持的9种基本图形和文字。使用如下已经封装好的React Native组件就可以直接使用了。
|
||||
|
||||
```javasctipt
|
||||
<ARKit.Box
|
||||
pos={{ x: 0, y: 0, z: 0 }}
|
||||
shape={{ width: 0.1, height: 0.1, length: 0.1, chamfer: 0.01 }}
|
||||
/>
|
||||
```
|
||||
|
||||
[几何构造](http://v.youku.com/v_show/id_XMzUxMjk3NjUxMg==.html)
|
||||
上面的一个视频片段是我们在跑起来Demo后的立体效果。可以很清楚地看到,ARKit感知到了房间这个立方体空间后所构建出来的AR的效果。
|
||||
|
||||
[平面识别](http://v.youku.com/v_show/id_XMzUxMjk3OTc0NA==.html)
|
||||
而最后的这段视频会更加有趣一些,中央的红圈的出现逻辑是停留在最近识别出的一个平面上。我们可以看到首先识别出了地面,红圈随地面而动;再移向桌面时,很快又识别出了桌面,重新生成了一个停留在桌面上的红圈。通过这一段可以看出无论是明暗划分明显的地面,还是堆满杂物的桌面,ARKit都可以很轻松的识别出来。
|
||||
|
||||
## 4. 总结
|
||||
苹果的ARKit对空间平面的感知能力胜过了一般的AR渲染库。而iPhone 6S就能跑的特性又让我们觉得AR其实并没有那么遥远。在此基础之上的React Native封装`react-native-arkit`,让我们通过JS就拥有操作ARKit的能力。这的确是一个快速上手ARKit的方式。
|
||||
|
||||
|
||||
## 5 更多讨论
|
||||
讨论地址是:[精读《快速上手构建ARKit应用》 · Issue #70 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/70)
|
||||
|
||||
如果你想参与讨论,请[点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,每周末发布。
|
||||
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 487 KiB |
|
Before Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 342 KiB |
|
Before Width: | Height: | Size: 818 KiB |
|
Before Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 556 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 128 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 414 KiB |
|
Before Width: | Height: | Size: 117 KiB |
|
Before Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 136 KiB |
|
Before Width: | Height: | Size: 125 KiB |
|
Before Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 119 KiB |
|
Before Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 203 KiB |
|
Before Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 396 KiB |
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 392 KiB |
|
Before Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 337 KiB |
|
Before Width: | Height: | Size: 294 KiB |
|
Before Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 9.7 KiB |
|
Before Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 312 KiB |
|
Before Width: | Height: | Size: 8.0 KiB |
|
Before Width: | Height: | Size: 121 KiB |
|
Before Width: | Height: | Size: 62 KiB |
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* 发布辅助脚本
|
||||
* @author 黄子毅
|
||||
*/
|
||||
|
||||
const fs = require("fs");
|
||||
|
||||
const dirs = [
|
||||
"前沿技术",
|
||||
"设计模式",
|
||||
"编译原理",
|
||||
"源码解读",
|
||||
"商业思考",
|
||||
"算法",
|
||||
];
|
||||
|
||||
dirs.forEach((dir) => {
|
||||
const readDir = fs.readdirSync(`./${dir}`);
|
||||
|
||||
console.log(`### ${dir}\n`);
|
||||
|
||||
readDir
|
||||
.sort((left, right) => left.split(".")[0] - right.split(".")[0])
|
||||
.forEach((dirName) => {
|
||||
console.log(
|
||||
`- <a href="./${dir}/${encodeURIComponent(dirName)}">${dirName.replace(
|
||||
".md",
|
||||
""
|
||||
)}</a>`
|
||||
);
|
||||
});
|
||||
|
||||
console.log("");
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "weekly",
|
||||
"version": "1.0.0",
|
||||
"description": "前端界的好文精读,每周更新!",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/dt-fe/weekly.git"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"bugs": {
|
||||
"url": "https://github.com/dt-fe/weekly/issues"
|
||||
},
|
||||
"homepage": "https://github.com/dt-fe/weekly#readme",
|
||||
"dependencies": {
|
||||
"esm": "^3.2.25",
|
||||
"husky": "^3.0.4",
|
||||
"lint-md-cli": "^0.1.1"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "npx lint-md ./"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,253 @@
|
||||
# 前端精读
|
||||
|
||||
<a href="https://travis-ci.org/ascoders/weekly">
|
||||
<img src="https://travis-ci.org/ascoders/weekly.svg?branch=v2" alt="CircleCI Status">
|
||||
</a>
|
||||
|
||||
前端界的好文精读,每周更新!
|
||||
|
||||
- [周刊参考池](https://github.com/dt-fe/weekly/issues/2)
|
||||
最新精读:<a href="./前沿技术/213.%E7%B2%BE%E8%AF%BB%E3%80%8APrisma%20%E7%9A%84%E4%BD%BF%E7%94%A8%E3%80%8B.md">213.精读《Prisma 的使用》</a>
|
||||
|
||||
素材来源:[周刊参考池](https://github.com/ascoders/weekly/issues/2)
|
||||
|
||||
现已涵盖:
|
||||
|
||||
- 结合大厂工作经验解读的 [前沿技术](./前沿技术),[源码解读](./源码解读)。
|
||||
- 逐渐加入一些后端技术解读。
|
||||
- 一些 [商业思考](./商业思考)。
|
||||
- 已完成 [编译原理](./编译原理)、[设计模式](./设计模式) 两大基础模块。
|
||||
|
||||
### 前沿技术
|
||||
|
||||
- <a href="./前沿技术/1.%E7%B2%BE%E8%AF%BB%E3%80%8Ajs%20%E6%A8%A1%E5%9D%97%E5%8C%96%E5%8F%91%E5%B1%95%E3%80%8B.md">1.精读《js 模块化发展》</a>
|
||||
- <a href="./前沿技术/2.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%A8%A1%E6%80%81%E6%A1%86%E7%9A%84%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5%E3%80%8B.md">2.精读《模态框的最佳实践》</a>
|
||||
- <a href="./前沿技术/3.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%89%8D%E5%90%8E%E7%AB%AF%E6%B8%B2%E6%9F%93%E4%B9%8B%E4%BA%89%E3%80%8B.md">3.精读《前后端渲染之争》</a>
|
||||
- <a href="./前沿技术/4.%E7%B2%BE%E8%AF%BB%E3%80%8AAsyncAwait%20%E4%BC%98%E8%B6%8A%E4%B9%8B%E5%A4%84%E3%80%8B.md">4.精读《AsyncAwait 优越之处》</a>
|
||||
- <a href="./前沿技术/5.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%B0%91%E5%B7%A5%E5%8F%94%E5%8D%95%E9%A1%B5%E6%95%B0%E6%8D%AE%E6%B5%81%E6%96%B9%E6%A1%88%E3%80%8B.md">5.精读《民工叔单页数据流方案》</a>
|
||||
- <a href="./前沿技术/6.%E7%B2%BE%E8%AF%BB%E3%80%8AJavaScript%20%E9%94%99%E8%AF%AF%E5%A0%86%E6%A0%88%E5%A4%84%E7%90%86%E3%80%8B.md">6.精读《JavaScript 错误堆栈处理》</a>
|
||||
- <a href="./前沿技术/7.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%AF%B7%E5%81%9C%E6%AD%A2%20css-in-js%20%E7%9A%84%E8%A1%8C%E4%B8%BA%E3%80%8B.md">7.精读《请停止 css-in-js 的行为》</a>
|
||||
- <a href="./前沿技术/8.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%85%A5%E5%9D%91%20React%20%E5%89%8D%E6%B2%A1%E6%9C%89%E4%BA%BA%E4%BC%9A%E5%91%8A%E8%AF%89%E4%BD%A0%E7%9A%84%E4%BA%8B%E3%80%8B.md">8.精读《入坑 React 前没有人会告诉你的事》</a>
|
||||
- <a href="./前沿技术/9.%E7%B2%BE%E8%AF%BB%E3%80%8AImmutable%20%E7%BB%93%E6%9E%84%E5%85%B1%E4%BA%AB%E3%80%8B.md">9.精读《Immutable 结构共享》</a>
|
||||
- <a href="./前沿技术/10.%E7%B2%BE%E8%AF%BB%E3%80%8AWeb%20Components%20%E7%9A%84%E5%9B%B0%E5%A2%83%E3%80%8B.md">10.精读《Web Components 的困境》</a>
|
||||
- <a href="./前沿技术/11.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%89%8D%E7%AB%AF%E8%B0%83%E8%AF%95%E6%8A%80%E5%B7%A7%E3%80%8B.md">11.精读《前端调试技巧》</a>
|
||||
- <a href="./前沿技术/12.%E7%B2%BE%E8%AF%BB%E3%80%8AReact%20%E9%AB%98%E9%98%B6%E7%BB%84%E4%BB%B6%E3%80%8B.md">12.精读《React 高阶组件》</a>
|
||||
- <a href="./前沿技术/13.%E7%B2%BE%E8%AF%BB%E3%80%8AThis%20%E5%B8%A6%E6%9D%A5%E7%9A%84%E5%9B%B0%E6%83%91%E3%80%8B.md">13.精读《This 带来的困惑》</a>
|
||||
- <a href="./前沿技术/14.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1%E4%B9%8B%20DCI%E3%80%8B.md">14.精读《架构设计之 DCI》</a>
|
||||
- <a href="./前沿技术/15.%E7%B2%BE%E8%AF%BB%E3%80%8ATC39%20%E4%B8%8E%20ECMAScript%20%E6%8F%90%E6%A1%88%E3%80%8B.md">15.精读《TC39 与 ECMAScript 提案》</a>
|
||||
- <a href="./前沿技术/16.%E7%B2%BE%E8%AF%BB%E3%80%8ACSS%20Animations%20vs%20Web%20Animations%20API%E3%80%8B.md">16.精读《CSS Animations vs Web Animations API》</a>
|
||||
- <a href="./前沿技术/17.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%A6%82%E4%BD%95%E5%AE%89%E5%85%A8%E5%9C%B0%E4%BD%BF%E7%94%A8%20React%20context%E3%80%8B.md">17.精读《如何安全地使用 React context》</a>
|
||||
- <a href="./前沿技术/18.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%AE%BE%E8%AE%A1%E5%AE%8C%E7%BE%8E%E7%9A%84%E6%97%A5%E6%9C%9F%E9%80%89%E6%8B%A9%E5%99%A8%E3%80%8B.md">18.精读《设计完美的日期选择器》</a>
|
||||
- <a href="./前沿技术/19.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%9C%80%E4%BD%B3%E5%89%8D%E7%AB%AF%E9%9D%A2%E8%AF%95%E9%A2%98%E3%80%8B%E5%8F%8A%E9%9D%A2%E8%AF%95%E5%AE%98%E6%8A%80%E5%B7%A7.md">19.精读《最佳前端面试题》及面试官技巧</a>
|
||||
- <a href="./前沿技术/20.%E7%B2%BE%E8%AF%BB%E3%80%8ANestjs%E3%80%8B%E6%96%87%E6%A1%A3.md">20.精读《Nestjs》文档</a>
|
||||
- <a href="./前沿技术/21.%E7%B2%BE%E8%AF%BB%E3%80%8AWeb%20fonts%3A%20when%20you%20need%20them%2C%20when%20you%20don%E2%80%99t%E3%80%8B.md">21.精读《Web fonts: when you need them, when you don’t》</a>
|
||||
- <a href="./前沿技术/22.%E7%B2%BE%E8%AF%BB%E3%80%8AV8%20%E5%BC%95%E6%93%8E%E7%89%B9%E6%80%A7%E5%B8%A6%E6%9D%A5%E7%9A%84%E7%9A%84%20JS%20%E6%80%A7%E8%83%BD%E5%8F%98%E5%8C%96%E3%80%8B.md">22.精读《V8 引擎特性带来的的 JS 性能变化》</a>
|
||||
- <a href="./前沿技术/23.%E7%B2%BE%E8%AF%BB%E3%80%8AAPI%20%E8%AE%BE%E8%AE%A1%E5%8E%9F%E5%88%99%E3%80%8B.md">23.精读《API 设计原则》</a>
|
||||
- <a href="./前沿技术/24.%E7%B2%BE%E8%AF%BB%E3%80%8A%E7%8E%B0%E4%BB%A3%20JavaScript%20%E6%A6%82%E8%A7%88%E3%80%8B.md">24.精读《现代 JavaScript 概览》</a>
|
||||
- <a href="./前沿技术/25.%E7%B2%BE%E8%AF%BB%E3%80%8Anull%20%3E%3D%200%3F%E3%80%8B.md">25.精读《null >= 0?》</a>
|
||||
- <a href="./前沿技术/26.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%8A%A0%E5%AF%86%E5%AA%92%E4%BD%93%E6%89%A9%E5%B1%95%E3%80%8B.md">26.精读《加密媒体扩展》</a>
|
||||
- <a href="./前沿技术/27.%E7%B2%BE%E8%AF%BB%E3%80%8Acss-in-js%20%E6%9D%80%E9%B8%A1%E7%94%A8%E7%89%9B%E5%88%80%E3%80%8B.md">27.精读《css-in-js 杀鸡用牛刀》</a>
|
||||
- <a href="./前沿技术/28.%E7%B2%BE%E8%AF%BB%E3%80%8A2017%20%E5%89%8D%E7%AB%AF%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96%E5%A4%87%E5%BF%98%E5%BD%95%E3%80%8B.md">28.精读《2017 前端性能优化备忘录》</a>
|
||||
- <a href="./前沿技术/29.%E7%B2%BE%E8%AF%BB%E3%80%8AJS%20%E4%B8%AD%E7%9A%84%E5%86%85%E5%AD%98%E7%AE%A1%E7%90%86%E3%80%8B.md">29.精读《JS 中的内存管理》</a>
|
||||
- <a href="./前沿技术/30.%E7%B2%BE%E8%AF%BB%E3%80%8AJavascript%20%E4%BA%8B%E4%BB%B6%E5%BE%AA%E7%8E%AF%E4%B8%8E%E5%BC%82%E6%AD%A5%E3%80%8B.md">30.精读《Javascript 事件循环与异步》</a>
|
||||
- <a href="./前沿技术/31.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%88%91%E4%B8%8D%E5%86%8D%E4%BD%BF%E7%94%A8%E9%AB%98%E9%98%B6%E7%BB%84%E4%BB%B6%E3%80%8B.md">31.精读《我不再使用高阶组件》</a>
|
||||
- <a href="./前沿技术/32.%E7%B2%BE%E8%AF%BB%E3%80%8AReact%20Router4.0%20%E8%BF%9B%E9%98%B6%E6%A6%82%E5%BF%B5%E3%80%8B.md">32.精读《React Router4.0 进阶概念》</a>
|
||||
- <a href="./前沿技术/33.%E7%B2%BE%E8%AF%BB%E3%80%8A30%20%E8%A1%8C%20js%20%E4%BB%A3%E7%A0%81%E5%88%9B%E5%BB%BA%E7%A5%9E%E7%BB%8F%E7%BD%91%E7%BB%9C%E3%80%8B.md">33.精读《30 行 js 代码创建神经网络》</a>
|
||||
- <a href="./前沿技术/34.%E7%B2%BE%E8%AF%BB%E3%80%8AReact%20%E4%BB%A3%E7%A0%81%E6%95%B4%E6%B4%81%E4%B9%8B%E9%81%93%E3%80%8B.md">34.精读《React 代码整洁之道》</a>
|
||||
- <a href="./前沿技术/35.%E7%B2%BE%E8%AF%BB%E3%80%8Adob%20-%20%E6%A1%86%E6%9E%B6%E5%AE%9E%E7%8E%B0%E3%80%8B.md">35.精读《dob - 框架实现》</a>
|
||||
- <a href="./前沿技术/36.%E7%B2%BE%E8%AF%BB%E3%80%8AWhen%20You%20%E2%80%9CGit%E2%80%9D%20in%20Trouble-%20a%20Version%20Control%20Story%E3%80%8B.md">36.精读《When You “Git” in Trouble- a Version Control Story》</a>
|
||||
- <a href="./前沿技术/37.%E7%B2%BE%E8%AF%BB%E3%80%8Ahow%20we%20position%20and%20what%20we%20compare%E3%80%8B.md">37.精读《how we position and what we compare》</a>
|
||||
- <a href="./前沿技术/38.%E7%B2%BE%E8%AF%BB%E3%80%8Adob%20-%20%E6%A1%86%E6%9E%B6%E4%BD%BF%E7%94%A8%E3%80%8B.md">38.精读《dob - 框架使用》</a>
|
||||
- <a href="./前沿技术/39.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%85%A8%E9%93%BE%E8%B7%AF%E4%BD%93%E9%AA%8C%E6%B5%8F%E8%A7%88%E5%99%A8%E6%8C%96%E7%9F%BF%E3%80%8B.md">39.精读《全链路体验浏览器挖矿》</a>
|
||||
- <a href="./前沿技术/40.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%88%9D%E6%8E%A2%20Reason%20%E4%B8%8E%20GraphQL%E3%80%8B.md">40.精读《初探 Reason 与 GraphQL》</a>
|
||||
- <a href="./前沿技术/41.%E7%B2%BE%E8%AF%BB%E3%80%8AAnt%20Design%203.0%20%E8%83%8C%E5%90%8E%E7%9A%84%E6%95%85%E4%BA%8B%E3%80%8B.md">41.精读《Ant Design 3.0 背后的故事》</a>
|
||||
- <a href="./前沿技术/42.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%89%8D%E7%AB%AF%E6%95%B0%E6%8D%AE%E6%B5%81%E5%93%B2%E5%AD%A6%E3%80%8B.md">42.精读《前端数据流哲学》</a>
|
||||
- <a href="./前沿技术/43.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%A2%9E%E5%BC%BA%E7%8E%B0%E5%AE%9E%E4%B8%8E%E5%8F%AF%E8%A7%86%E5%8C%96%E3%80%8B.md">43.精读《增强现实与可视化》</a>
|
||||
- <a href="./前沿技术/44.%E7%B2%BE%E8%AF%BB%E3%80%8ARekit%20Studio%E3%80%8B.md">44.精读《Rekit Studio》</a>
|
||||
- <a href="./前沿技术/45.%E7%B2%BE%E8%AF%BB%E3%80%8AReact's%20new%20Context%20API%E3%80%8B.md">45.精读《React's new Context API》</a>
|
||||
- <a href="./前沿技术/46.%E7%B2%BE%E8%AF%BB%E3%80%8Areact-rxjs%E3%80%8B.md">46.精读《react-rxjs》</a>
|
||||
- <a href="./前沿技术/47.%E7%B2%BE%E8%AF%BB%E3%80%8Awebpack4.0%20%E5%8D%87%E7%BA%A7%E6%8C%87%E5%8D%97%E3%80%8B.md">47.精读《webpack4.0 升级指南》</a>
|
||||
- <a href="./前沿技术/49.%E7%B2%BE%E8%AF%BB%E3%80%8ACompilers%20are%20the%20New%20Frameworks%E3%80%8B.md">49.精读《Compilers are the New Frameworks》</a>
|
||||
- <a href="./前沿技术/50.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%BF%AB%E9%80%9F%E4%B8%8A%E6%89%8B%E6%9E%84%E5%BB%BA%20ARKit%20%E5%BA%94%E7%94%A8%E3%80%8B.md">50.精读《快速上手构建 ARKit 应用》</a>
|
||||
- <a href="./前沿技术/51.%E7%B2%BE%E8%AF%BB%E3%80%8AElements%20of%20Web%20Dev%E3%80%8B.md">51.精读《Elements of Web Dev》</a>
|
||||
- <a href="./前沿技术/52.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%9B%BE%E8%A7%A3%20ES%20%E6%A8%A1%E5%9D%97%E3%80%8B.md">52.精读《图解 ES 模块》</a>
|
||||
- <a href="./前沿技术/53.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%8F%92%E4%BB%B6%E5%8C%96%E6%80%9D%E7%BB%B4%E3%80%8B.md">53.精读《插件化思维》</a>
|
||||
- <a href="./前沿技术/54.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%9C%A8%E6%B5%8F%E8%A7%88%E5%99%A8%E8%BF%90%E8%A1%8C%20serverRender%E3%80%8B.md">54.精读《在浏览器运行 serverRender》</a>
|
||||
- <a href="./前沿技术/55.%E7%B2%BE%E8%AF%BB%E3%80%8Aasync%20await%20%E6%98%AF%E6%8A%8A%E5%8F%8C%E5%88%83%E5%89%91%E3%80%8B.md">55.精读《async await 是把双刃剑》</a>
|
||||
- <a href="./前沿技术/56.%E7%B2%BE%E8%AF%BB%E3%80%8A%E9%87%8D%E6%96%B0%E6%80%9D%E8%80%83%20Redux%E3%80%8B.md">56.精读《重新思考 Redux》</a>
|
||||
- <a href="./前沿技术/57.%E7%B2%BE%E8%AF%BB%E3%80%8A%E7%8E%B0%E4%BB%A3%20js%20%E6%A1%86%E6%9E%B6%E5%AD%98%E5%9C%A8%E7%9A%84%E6%A0%B9%E6%9C%AC%E5%8E%9F%E5%9B%A0%E3%80%8B.md">57.精读《现代 js 框架存在的根本原因》</a>
|
||||
- <a href="./前沿技术/58.%E7%B2%BE%E8%AF%BB%E3%80%8ATypescript2.0%20-%202.9%E3%80%8B.md">58.精读《Typescript2.0 - 2.9》</a>
|
||||
- <a href="./前沿技术/59.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%A6%82%E4%BD%95%E5%88%A9%E7%94%A8%20Nodejs%20%E7%9B%91%E5%90%AC%E6%96%87%E4%BB%B6%E5%A4%B9%E3%80%8B.md">59.精读《如何利用 Nodejs 监听文件夹》</a>
|
||||
- <a href="./前沿技术/60.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%A6%82%E4%BD%95%E5%9C%A8%20nodejs%20%E4%BD%BF%E7%94%A8%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8F%E3%80%8B.md">60.精读《如何在 nodejs 使用环境变量》</a>
|
||||
- <a href="./前沿技术/61.%E7%B2%BE%E8%AF%BB%E3%80%8AReact%20%E5%85%AB%E7%A7%8D%E6%9D%A1%E4%BB%B6%E6%B8%B2%E6%9F%93%E3%80%8B.md">61.精读《React 八种条件渲染》</a>
|
||||
- <a href="./前沿技术/62.%E7%B2%BE%E8%AF%BB%E3%80%8AJS%20%E5%BC%95%E6%93%8E%E5%9F%BA%E7%A1%80%E4%B9%8B%20Shapes%20and%20Inline%20Caches%E3%80%8B.md">62.精读《JS 引擎基础之 Shapes and Inline Caches》</a>
|
||||
- <a href="./前沿技术/63.%E7%B2%BE%E8%AF%BB%E3%80%8AReact%20%E7%9A%84%E5%A4%9A%E6%80%81%E6%80%A7%E3%80%8B.md">63.精读《React 的多态性》</a>
|
||||
- <a href="./前沿技术/68.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%A1%A1%E9%87%8F%E7%94%A8%E6%88%B7%E4%BD%93%E9%AA%8C%E3%80%8B.md">68.精读《衡量用户体验》</a>
|
||||
- <a href="./前沿技术/69.%E7%B2%BE%E8%AF%BB%E3%80%8ASQL%20vs%20Flux%E3%80%8B.md">69.精读《SQL vs Flux》</a>
|
||||
- <a href="./前沿技术/72.%E7%B2%BE%E8%AF%BB%E3%80%8AREST%2C%20GraphQL%2C%20Webhooks%2C%20%26%20gRPC%20%E5%A6%82%E4%BD%95%E9%80%89%E5%9E%8B%E3%80%8B.md">72.精读《REST, GraphQL, Webhooks, & gRPC 如何选型》</a>
|
||||
- <a href="./前沿技术/74.%E7%B2%BE%E8%AF%BB%E3%80%8A12%20%E4%B8%AA%E8%AF%84%E4%BC%B0%20JS%20%E5%BA%93%E4%BD%A0%E9%9C%80%E8%A6%81%E5%85%B3%E5%BF%83%E7%9A%84%E4%BA%8B%E3%80%8B.md">74.精读《12 个评估 JS 库你需要关心的事》</a>
|
||||
- <a href="./前沿技术/76.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%B0%88%E8%B0%88%20Web%20Workers%E3%80%8B.md">76.精读《谈谈 Web Workers》</a>
|
||||
- <a href="./前沿技术/77.%E7%B2%BE%E8%AF%BB%E3%80%8A%E7%94%A8%20Reduce%20%E5%AE%9E%E7%8E%B0%20Promise%20%E4%B8%B2%E8%A1%8C%E6%89%A7%E8%A1%8C%E3%80%8B.md">77.精读《用 Reduce 实现 Promise 串行执行》</a>
|
||||
- <a href="./前沿技术/79.%E7%B2%BE%E8%AF%BB%E3%80%8AReact%20Hooks%E3%80%8B.md">79.精读《React Hooks》</a>
|
||||
- <a href="./前沿技术/80.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%80%8E%E4%B9%88%E7%94%A8%20React%20Hooks%20%E9%80%A0%E8%BD%AE%E5%AD%90%E3%80%8B.md">80.精读《怎么用 React Hooks 造轮子》</a>
|
||||
- <a href="./前沿技术/81.%E7%B2%BE%E8%AF%BB%E3%80%8A%E4%BD%BF%E7%94%A8%20CSS%20%E5%B1%9E%E6%80%A7%E9%80%89%E6%8B%A9%E5%99%A8%E3%80%8B.md">81.精读《使用 CSS 属性选择器》</a>
|
||||
- <a href="./前沿技术/83.%E7%B2%BE%E8%AF%BB%E3%80%8AReact16%20%E6%96%B0%E7%89%B9%E6%80%A7%E3%80%8B.md">83.精读《React16 新特性》</a>
|
||||
- <a href="./前沿技术/84.%E7%B2%BE%E8%AF%BB%E3%80%8ATypescript%203.2%20%E6%96%B0%E7%89%B9%E6%80%A7%E3%80%8B.md">84.精读《Typescript 3.2 新特性》</a>
|
||||
- <a href="./前沿技术/86.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%9B%BD%E9%99%85%E5%8C%96%E5%B8%83%E5%B1%80%20-%20Logical%20Properties%E3%80%8B.md">86.精读《国际化布局 - Logical Properties》</a>
|
||||
- <a href="./前沿技术/87.%E7%B2%BE%E8%AF%BB%E3%80%8AsetState%20%E5%81%9A%E4%BA%86%E4%BB%80%E4%B9%88%E3%80%8B.md">87.精读《setState 做了什么》</a>
|
||||
- <a href="./前沿技术/88.%E7%B2%BE%E8%AF%BB%E3%80%8ACaches%20API%E3%80%8B.md">88.精读《Caches API》</a>
|
||||
- <a href="./前沿技术/89.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%A6%82%E4%BD%95%E7%BC%96%E8%AF%91%E5%89%8D%E7%AB%AF%E9%A1%B9%E7%9B%AE%E4%B8%8E%E7%BB%84%E4%BB%B6%E3%80%8B.md">89.精读《如何编译前端项目与组件》</a>
|
||||
- <a href="./前沿技术/91.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%AD%A3%E5%88%99%20ES2018%E3%80%8B.md">91.精读《正则 ES2018》</a>
|
||||
- <a href="./前沿技术/94.%E7%B2%BE%E8%AF%BB%E3%80%8AServerless%20%E7%BB%99%E5%89%8D%E7%AB%AF%E5%B8%A6%E6%9D%A5%E4%BA%86%E4%BB%80%E4%B9%88%E3%80%8B.md">94.精读《Serverless 给前端带来了什么》</a>
|
||||
- <a href="./前沿技术/95.%E7%B2%BE%E8%AF%BB%E3%80%8AFunction%20VS%20Class%20%E7%BB%84%E4%BB%B6%E3%80%8B.md">95.精读《Function VS Class 组件》</a>
|
||||
- <a href="./前沿技术/96.%E7%B2%BE%E8%AF%BB%E3%80%8AuseEffect%20%E5%AE%8C%E5%85%A8%E6%8C%87%E5%8D%97%E3%80%8B.md">96.精读《useEffect 完全指南》</a>
|
||||
- <a href="./前沿技术/97.%E7%B2%BE%E8%AF%BB%E3%80%8A%E7%BC%96%E5%86%99%E6%9C%89%E5%BC%B9%E6%80%A7%E7%9A%84%E7%BB%84%E4%BB%B6%E3%80%8B.md">97.精读《编写有弹性的组件》</a>
|
||||
- <a href="./前沿技术/99.%E7%B2%BE%E8%AF%BB%E3%80%8AScheduling%20in%20React%E3%80%8B.md">99.精读《Scheduling in React》</a>
|
||||
- <a href="./前沿技术/100.%E7%B2%BE%E8%AF%BB%E3%80%8AV8%20%E5%BC%95%E6%93%8E%20Lazy%20Parsing%E3%80%8B.md">100.精读《V8 引擎 Lazy Parsing》</a>
|
||||
- <a href="./前沿技术/101.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%8C%81%E7%BB%AD%E9%9B%86%E6%88%90%20vs%20%E6%8C%81%E7%BB%AD%E4%BA%A4%E4%BB%98%20vs%20%E6%8C%81%E7%BB%AD%E9%83%A8%E7%BD%B2%E3%80%8B.md">101.精读《持续集成 vs 持续交付 vs 持续部署》</a>
|
||||
- <a href="./前沿技术/102.%E7%B2%BE%E8%AF%BB%E3%80%8AMonorepo%20%E7%9A%84%E4%BC%98%E5%8A%BF%E3%80%8B.md">102.精读《Monorepo 的优势》</a>
|
||||
- <a href="./前沿技术/104.%E7%B2%BE%E8%AF%BB%E3%80%8AFunction%20Component%20%E5%85%A5%E9%97%A8%E3%80%8B.md">104.精读《Function Component 入门》</a>
|
||||
- <a href="./前沿技术/105.%E7%B2%BE%E8%AF%BB%E3%80%8AWhat's%20new%20in%20javascript%E3%80%8B.md">105.精读《What's new in javascript》</a>
|
||||
- <a href="./前沿技术/107.%E7%B2%BE%E8%AF%BB%E3%80%8AOptional%20chaining%E3%80%8B.md">107.精读《Optional chaining》</a>
|
||||
- <a href="./前沿技术/109.%E7%B2%BE%E8%AF%BB%E3%80%8AVue3.0%20Function%20API%E3%80%8B.md">109.精读《Vue3.0 Function API》</a>
|
||||
- <a href="./前沿技术/111.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%89%8D%E7%AB%AF%E6%9C%AA%E6%9D%A5%E5%B1%95%E6%9C%9B%E3%80%8B.md">111.精读《前端未来展望》</a>
|
||||
- <a href="./前沿技术/112.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%BA%90%E7%A0%81%E5%AD%A6%E4%B9%A0%E3%80%8B.md">112.精读《源码学习》</a>
|
||||
- <a href="./前沿技术/113.%E7%B2%BE%E8%AF%BB%E3%80%8ANodejs%20V12%E3%80%8B.md">113.精读《Nodejs V12》</a>
|
||||
- <a href="./前沿技术/117.%E7%B2%BE%E8%AF%BB%E3%80%8ATableau%20%E6%8E%A2%E7%B4%A2%E5%BC%8F%E6%A8%A1%E5%9E%8B%E3%80%8B.md">117.精读《Tableau 探索式模型》</a>
|
||||
- <a href="./前沿技术/118.%E7%B2%BE%E8%AF%BB%E3%80%8A%E4%BD%BF%E7%94%A8%20css%20%E5%8F%98%E9%87%8F%E7%94%9F%E6%88%90%E9%A2%9C%E8%89%B2%E4%B8%BB%E9%A2%98%E3%80%8B.md">118.精读《使用 css 变量生成颜色主题》</a>
|
||||
- <a href="./前沿技术/119.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%89%8D%E7%AB%AF%E6%B7%B1%E6%B0%B4%E5%8C%BA%E3%80%8B.md">119.精读《前端深水区》</a>
|
||||
- <a href="./前沿技术/120.%E7%B2%BE%E8%AF%BB%E3%80%8AReact%20Hooks%20%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5%E3%80%8B.md">120.精读《React Hooks 最佳实践》</a>
|
||||
- <a href="./前沿技术/121.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%89%8D%E7%AB%AF%E4%B8%8E%20BI%E3%80%8B.md">121.精读《前端与 BI》</a>
|
||||
- <a href="./前沿技术/123.%E7%B2%BE%E8%AF%BB%E3%80%8A%E7%94%A8%20Babel%20%E5%88%9B%E9%80%A0%E8%87%AA%E5%AE%9A%E4%B9%89%20JS%20%E8%AF%AD%E6%B3%95%E3%80%8B.md">123.精读《用 Babel 创造自定义 JS 语法》</a>
|
||||
- <a href="./前沿技术/124.%E7%B2%BE%E8%AF%BB%E3%80%8A%E7%94%A8%20css%20grid%20%E9%87%8D%E6%96%B0%E6%80%9D%E8%80%83%E5%B8%83%E5%B1%80%E3%80%8B.md">124.精读《用 css grid 重新思考布局》</a>
|
||||
- <a href="./前沿技术/125.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%B7%B1%E5%BA%A6%E5%AD%A6%E4%B9%A0%20-%20%E5%87%BD%E6%95%B0%E5%BC%8F%E4%B9%8B%E7%BE%8E%E3%80%8B.md">125.精读《深度学习 - 函数式之美》</a>
|
||||
- <a href="./前沿技术/126.%E7%B2%BE%E8%AF%BB%E3%80%8ANuxtjs%E3%80%8B.md">126.精读《Nuxtjs》</a>
|
||||
- <a href="./前沿技术/127.%E7%B2%BE%E8%AF%BB%E3%80%8AReact%20Conf%202019%20-%20Day1%E3%80%8B.md">127.精读《React Conf 2019 - Day1》</a>
|
||||
- <a href="./前沿技术/129.%E7%B2%BE%E8%AF%BB%E3%80%8AReact%20Conf%202019%20-%20Day2%E3%80%8B.md">129.精读《React Conf 2019 - Day2》</a>
|
||||
- <a href="./前沿技术/132.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%AD%A3%E4%BA%A4%E7%9A%84%20React%20%E7%BB%84%E4%BB%B6%E3%80%8B.md">132.精读《正交的 React 组件》</a>
|
||||
- <a href="./前沿技术/133.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%AF%BB%E6%89%BE%E6%A1%86%E6%9E%B6%E8%AE%BE%E8%AE%A1%E7%9A%84%E5%B9%B3%E8%A1%A1%E7%82%B9%E3%80%8B.md">133.精读《寻找框架设计的平衡点》</a>
|
||||
- <a href="./前沿技术/134.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%88%91%E5%9C%A8%E9%98%BF%E9%87%8C%E6%95%B0%E6%8D%AE%E4%B8%AD%E5%8F%B0%E5%A4%A7%E5%89%8D%E7%AB%AF%E3%80%8B.md">134.精读《我在阿里数据中台大前端》</a>
|
||||
- <a href="./前沿技术/138.%E7%B2%BE%E8%AF%BB%E3%80%8A%E7%B2%BE%E9%80%9A%20console.log%E3%80%8B.md">138.精读《精通 console.log》</a>
|
||||
- <a href="./前沿技术/139.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%89%8B%E5%86%99%20JSON%20Parser%E3%80%8B.md">139.精读《手写 JSON Parser》</a>
|
||||
- <a href="./前沿技术/140.%E7%B2%BE%E8%AF%BB%E3%80%8A%E7%BB%93%E5%90%88%20React%20%E4%BD%BF%E7%94%A8%E5%8E%9F%E7%94%9F%20Drag%20Drop%20API%E3%80%8B.md">140.精读《结合 React 使用原生 Drag Drop API》</a>
|
||||
- <a href="./前沿技术/141.%E7%B2%BE%E8%AF%BB%E3%80%8AuseRef%20%E4%B8%8E%20createRef%20%E7%9A%84%E5%8C%BA%E5%88%AB%E3%80%8B.md">141.精读《useRef 与 createRef 的区别》</a>
|
||||
- <a href="./前沿技术/142.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%A6%82%E4%BD%95%E5%81%9A%E5%A5%BD%20CodeReview%E3%80%8B.md">142.精读《如何做好 CodeReview》</a>
|
||||
- <a href="./前沿技术/143.%E7%B2%BE%E8%AF%BB%E3%80%8ASuspense%20%E6%94%B9%E5%8F%98%E5%BC%80%E5%8F%91%E6%96%B9%E5%BC%8F%E3%80%8B.md">143.精读《Suspense 改变开发方式》</a>
|
||||
- <a href="./前沿技术/144.%E7%B2%BE%E8%AF%BB%E3%80%8AWebpack5%20%E6%96%B0%E7%89%B9%E6%80%A7%20-%20%E6%A8%A1%E5%9D%97%E8%81%94%E9%82%A6%E3%80%8B.md">144.精读《Webpack5 新特性 - 模块联邦》</a>
|
||||
- <a href="./前沿技术/145.%E7%B2%BE%E8%AF%BB%E3%80%8AReact%20Router%20v6%E3%80%8B.md">145.精读《React Router v6》</a>
|
||||
- <a href="./前沿技术/146.%E7%B2%BE%E8%AF%BB%E3%80%8AReact%20Hooks%20%E6%95%B0%E6%8D%AE%E6%B5%81%E3%80%8B.md">146.精读《React Hooks 数据流》</a>
|
||||
- <a href="./前沿技术/147.%20%E7%B2%BE%E8%AF%BB%E3%80%8A%40types%20react%20%E5%80%BC%E5%BE%97%E6%B3%A8%E6%84%8F%E7%9A%84%20TS%20%E6%8A%80%E5%B7%A7%E3%80%8B.md">147. 精读《@types react 值得注意的 TS 技巧》</a>
|
||||
- <a href="./前沿技术/148.%20%E7%B2%BE%E8%AF%BB%E3%80%8AReact%20Error%20Boundaries%E3%80%8B.md">148. 精读《React Error Boundaries》</a>
|
||||
- <a href="./前沿技术/149.%20%E7%B2%BE%E8%AF%BB%E3%80%8AReact%20%E6%80%A7%E8%83%BD%E8%B0%83%E8%AF%95%E3%80%8B.md">149. 精读《React 性能调试》</a>
|
||||
- <a href="./前沿技术/150.%20%E7%B2%BE%E8%AF%BB%E3%80%8ADeno%201.0%20%E4%BD%A0%E9%9C%80%E8%A6%81%E4%BA%86%E8%A7%A3%E7%9A%84%E3%80%8B.md">150. 精读《Deno 1.0 你需要了解的》</a>
|
||||
- <a href="./前沿技术/152.%20%E7%B2%BE%E8%AF%BB%E3%80%8Arecoil%E3%80%8B.md">152. 精读《recoil》</a>
|
||||
- <a href="./前沿技术/153.%20%E7%B2%BE%E8%AF%BB%E3%80%8Asnowpack%E3%80%8B.md">153. 精读《snowpack》</a>
|
||||
- <a href="./前沿技术/154.%20%E7%B2%BE%E8%AF%BB%E3%80%8A%E7%94%A8%20React%20%E5%81%9A%E6%8C%89%E9%9C%80%E6%B8%B2%E6%9F%93%E3%80%8B.md">154. 精读《用 React 做按需渲染》</a>
|
||||
- <a href="./前沿技术/157.%20%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%A6%82%E4%BD%95%E6%AF%94%E8%BE%83%20Object%20%E5%AF%B9%E8%B1%A1%E3%80%8B.md">157. 精读《如何比较 Object 对象》</a>
|
||||
- <a href="./前沿技术/158.%20%E7%B2%BE%E8%AF%BB%E3%80%8ATypescript%204%E3%80%8B.md">158. 精读《Typescript 4》</a>
|
||||
- <a href="./前沿技术/159.%20%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%AF%B9%E4%BD%8E%E4%BB%A3%E7%A0%81%E6%90%AD%E5%BB%BA%E7%9A%84%E7%90%86%E8%A7%A3%E3%80%8B.md">159. 精读《对低代码搭建的理解》</a>
|
||||
- <a href="./前沿技术/160.%20%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%87%BD%E6%95%B0%E7%BC%93%E5%AD%98%E3%80%8B.md">160. 精读《函数缓存》</a>
|
||||
- <a href="./前沿技术/161.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%8F%AF%E8%A7%86%E5%8C%96%E6%90%AD%E5%BB%BA%E6%80%9D%E8%80%83%20-%20%E5%AF%8C%E6%96%87%E6%9C%AC%E6%90%AD%E5%BB%BA%E3%80%8B.md">161.精读《可视化搭建思考 - 富文本搭建》</a>
|
||||
- <a href="./前沿技术/162.%E7%B2%BE%E8%AF%BB%E3%80%8ATasks%2C%20microtasks%2C%20queues%20and%20schedules%E3%80%8B.md">162.精读《Tasks, microtasks, queues and schedules》</a>
|
||||
- <a href="./前沿技术/163.%E7%B2%BE%E8%AF%BB%E3%80%8ASpring%20%E6%A6%82%E5%BF%B5%E3%80%8B.md">163.精读《Spring 概念》</a>
|
||||
- <a href="./前沿技术/164.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%95%B0%E6%8D%AE%E6%90%AD%E5%BB%BA%E5%BC%95%E6%93%8E%20bi-designer%20API-%E8%AE%BE%E8%AE%A1%E5%99%A8%E3%80%8B.md">164.精读《数据搭建引擎 bi-designer API-设计器》</a>
|
||||
- <a href="./前沿技术/165.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%95%B0%E6%8D%AE%E6%90%AD%E5%BB%BA%E5%BC%95%E6%93%8E%20bi-designer%20API-%E7%BB%84%E4%BB%B6%E3%80%8B.md">165.精读《数据搭建引擎 bi-designer API-组件》</a>
|
||||
- <a href="./前沿技术/166.%E7%B2%BE%E8%AF%BB%E3%80%8ABI%20%E6%90%AD%E5%BB%BA%20-%20%E7%AD%9B%E9%80%89%E6%9D%A1%E4%BB%B6%E3%80%8B.md">166.精读《BI 搭建 - 筛选条件》</a>
|
||||
- <a href="./前沿技术/190.%E7%B2%BE%E8%AF%BB%E3%80%8ADOM%20diff%20%E5%8E%9F%E7%90%86%E8%AF%A6%E8%A7%A3%E3%80%8B.md">190.精读《DOM diff 原理详解》</a>
|
||||
- <a href="./前沿技术/191.%E7%B2%BE%E8%AF%BB%E3%80%8A%E9%AB%98%E6%80%A7%E8%83%BD%E8%A1%A8%E6%A0%BC%E3%80%8B.md">191.精读《高性能表格》</a>
|
||||
- <a href="./前沿技术/192.%E7%B2%BE%E8%AF%BB%E3%80%8ADOM%20diff%20%E6%9C%80%E9%95%BF%E4%B8%8A%E5%8D%87%E5%AD%90%E5%BA%8F%E5%88%97%E3%80%8B.md">192.精读《DOM diff 最长上升子序列》</a>
|
||||
- <a href="./前沿技术/193.%E7%B2%BE%E8%AF%BB%E3%80%8AReact%20Server%20Component%E3%80%8B.md">193.精读《React Server Component》</a>
|
||||
- <a href="./前沿技术/194.%E7%B2%BE%E8%AF%BB%E3%80%8A%E7%AE%97%E6%B3%95%E5%9F%BA%E7%A1%80%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E3%80%8B.md">194.精读《算法基础数据结构》</a>
|
||||
- <a href="./前沿技术/195.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%96%B0%E4%B8%80%E4%BB%A3%E5%89%8D%E7%AB%AF%E6%9E%84%E5%BB%BA%E5%B7%A5%E5%85%B7%E5%AF%B9%E6%AF%94%E3%80%8B.md">195.精读《新一代前端构建工具对比》</a>
|
||||
- <a href="./前沿技术/196.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%89%8D%E7%AB%AF%E8%81%8C%E4%B8%9A%E8%A7%84%E5%88%92%20-%202021%20%E5%B9%B4%E3%80%8B.md">196.精读《前端职业规划 - 2021 年》</a>
|
||||
- <a href="./前沿技术/197.%E7%B2%BE%E8%AF%BB%E3%80%8A%E4%BD%8E%E4%BB%A3%E7%A0%81%E9%80%BB%E8%BE%91%E7%BC%96%E6%8E%92%E3%80%8B.md">197.精读《低代码逻辑编排》</a>
|
||||
- <a href="./前沿技术/202.%E7%B2%BE%E8%AF%BB%E3%80%8AReact%2018%E3%80%8B.md">202.精读《React 18》</a>
|
||||
- <a href="./前沿技术/204.%E7%B2%BE%E8%AF%BB%E3%80%8A%E9%BB%98%E8%AE%A4%E3%80%81%E5%91%BD%E5%90%8D%E5%AF%BC%E5%87%BA%E7%9A%84%E5%8C%BA%E5%88%AB%E3%80%8B.md">204.精读《默认、命名导出的区别》</a>
|
||||
- <a href="./前沿技术/205.%E7%B2%BE%E8%AF%BB%E3%80%8AJS%20with%20%E8%AF%AD%E6%B3%95%E3%80%8B.md">205.精读《JS with 语法》</a>
|
||||
- <a href="./前沿技术/206.%E7%B2%BE%E8%AF%BB%E3%80%8A%E4%B8%80%E7%A7%8D%20Hooks%20%E6%95%B0%E6%8D%AE%E6%B5%81%E7%AE%A1%E7%90%86%E6%96%B9%E6%A1%88%E3%80%8B.md">206.精读《一种 Hooks 数据流管理方案》</a>
|
||||
- <a href="./前沿技术/207.%E7%B2%BE%E8%AF%BB%E3%80%8ATypescript%20infer%20%E5%85%B3%E9%94%AE%E5%AD%97%E3%80%8B.md">207.精读《Typescript infer 关键字》</a>
|
||||
- <a href="./前沿技术/208.%E7%B2%BE%E8%AF%BB%E3%80%8ATypescript%204.4%E3%80%8B.md">208.精读《Typescript 4.4》</a>
|
||||
- <a href="./前沿技术/209.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%8D%95%E8%8E%B7%E6%89%80%E6%9C%89%E5%BC%82%E6%AD%A5%20error%E3%80%8B.md">209.精读《捕获所有异步 error》</a>
|
||||
- <a href="./前沿技术/210.%E7%B2%BE%E8%AF%BB%E3%80%8Aclass%20static%20block%E3%80%8B.md">210.精读《class static block》</a>
|
||||
- <a href="./前沿技术/211.%E7%B2%BE%E8%AF%BB%E3%80%8AMicrosoft%20Power%20Fx%E3%80%8B.md">211.精读《Microsoft Power Fx》</a>
|
||||
- <a href="./前沿技术/212.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%8F%AF%E7%BB%B4%E6%8A%A4%E6%80%A7%E6%80%9D%E8%80%83%E3%80%8B.md">212.精读《可维护性思考》</a>
|
||||
- <a href="./前沿技术/213.%E7%B2%BE%E8%AF%BB%E3%80%8APrisma%20%E7%9A%84%E4%BD%BF%E7%94%A8%E3%80%8B.md">213.精读《Prisma 的使用》</a>
|
||||
|
||||
### 设计模式
|
||||
|
||||
- <a href="./设计模式/167.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20-%20Abstract%20Factory%20%E6%8A%BD%E8%B1%A1%E5%B7%A5%E5%8E%82%E3%80%8B.md">167.精读《设计模式 - Abstract Factory 抽象工厂》</a>
|
||||
- <a href="./设计模式/168.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20-%20Builder%20%E7%94%9F%E6%88%90%E5%99%A8%E3%80%8B.md">168.精读《设计模式 - Builder 生成器》</a>
|
||||
- <a href="./设计模式/169.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20-%20Factory%20Method%20%E5%B7%A5%E5%8E%82%E6%96%B9%E6%B3%95%E3%80%8B.md">169.精读《设计模式 - Factory Method 工厂方法》</a>
|
||||
- <a href="./设计模式/170.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20-%20Prototype%20%E5%8E%9F%E5%9E%8B%E6%A8%A1%E5%BC%8F%E3%80%8B.md">170.精读《设计模式 - Prototype 原型模式》</a>
|
||||
- <a href="./设计模式/171.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20-%20Singleton%20%E5%8D%95%E4%BE%8B%E6%A8%A1%E5%BC%8F%E3%80%8B.md">171.精读《设计模式 - Singleton 单例模式》</a>
|
||||
- <a href="./设计模式/172.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20-%20Adapter%20%E9%80%82%E9%85%8D%E5%99%A8%E6%A8%A1%E5%BC%8F%E3%80%8B.md">172.精读《设计模式 - Adapter 适配器模式》</a>
|
||||
- <a href="./设计模式/173.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20-%20Bridge%20%E6%A1%A5%E6%8E%A5%E6%A8%A1%E5%BC%8F%E3%80%8B.md">173.精读《设计模式 - Bridge 桥接模式》</a>
|
||||
- <a href="./设计模式/174.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20-%20Composite%20%E7%BB%84%E5%90%88%E6%A8%A1%E5%BC%8F%E3%80%8B.md">174.精读《设计模式 - Composite 组合模式》</a>
|
||||
- <a href="./设计模式/175.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20-%20Decorator%20%E8%A3%85%E9%A5%B0%E5%99%A8%E6%A8%A1%E5%BC%8F%E3%80%8B.md">175.精读《设计模式 - Decorator 装饰器模式》</a>
|
||||
- <a href="./设计模式/176.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20-%20Facade%20%E5%A4%96%E8%A7%82%E6%A8%A1%E5%BC%8F%E3%80%8B.md">176.精读《设计模式 - Facade 外观模式》</a>
|
||||
- <a href="./设计模式/177.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20-%20Flyweight%20%E4%BA%AB%E5%85%83%E6%A8%A1%E5%BC%8F%E3%80%8B.md">177.精读《设计模式 - Flyweight 享元模式》</a>
|
||||
- <a href="./设计模式/178.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20-%20Proxy%20%E4%BB%A3%E7%90%86%E6%A8%A1%E5%BC%8F%E3%80%8B.md">178.精读《设计模式 - Proxy 代理模式》</a>
|
||||
- <a href="./设计模式/179.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20-%20Chain%20of%20Responsibility%20%E8%81%8C%E8%B4%A3%E9%93%BE%E6%A8%A1%E5%BC%8F%E3%80%8B.md">179.精读《设计模式 - Chain of Responsibility 职责链模式》</a>
|
||||
- <a href="./设计模式/180.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20-%20Command%20%E5%91%BD%E4%BB%A4%E6%A8%A1%E5%BC%8F%E3%80%8B.md">180.精读《设计模式 - Command 命令模式》</a>
|
||||
- <a href="./设计模式/181.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20-%20Interpreter%20%E8%A7%A3%E9%87%8A%E5%99%A8%E6%A8%A1%E5%BC%8F%E3%80%8B.md">181.精读《设计模式 - Interpreter 解释器模式》</a>
|
||||
- <a href="./设计模式/182.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20-%20Iterator%20%E8%BF%AD%E4%BB%A3%E5%99%A8%E6%A8%A1%E5%BC%8F%E3%80%8B.md">182.精读《设计模式 - Iterator 迭代器模式》</a>
|
||||
- <a href="./设计模式/183.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20-%20Mediator%20%E4%B8%AD%E4%BB%8B%E8%80%85%E6%A8%A1%E5%BC%8F%E3%80%8B.md">183.精读《设计模式 - Mediator 中介者模式》</a>
|
||||
- <a href="./设计模式/184.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20-%20Memoto%20%E5%A4%87%E5%BF%98%E5%BD%95%E6%A8%A1%E5%BC%8F%E3%80%8B.md">184.精读《设计模式 - Memoto 备忘录模式》</a>
|
||||
- <a href="./设计模式/185.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20-%20Observer%20%E8%A7%82%E5%AF%9F%E8%80%85%E6%A8%A1%E5%BC%8F%E3%80%8B.md">185.精读《设计模式 - Observer 观察者模式》</a>
|
||||
- <a href="./设计模式/186.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20-%20State%20%E7%8A%B6%E6%80%81%E6%A8%A1%E5%BC%8F%E3%80%8B.md">186.精读《设计模式 - State 状态模式》</a>
|
||||
- <a href="./设计模式/187.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20-%20Strategy%20%E7%AD%96%E7%95%A5%E6%A8%A1%E5%BC%8F%E3%80%8B.md">187.精读《设计模式 - Strategy 策略模式》</a>
|
||||
- <a href="./设计模式/188.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20-%20Template%20Method%20%E6%A8%A1%E7%89%88%E6%A8%A1%E5%BC%8F%E3%80%8B.md">188.精读《设计模式 - Template Method 模版模式》</a>
|
||||
- <a href="./设计模式/189.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20-%20Visitor%20%E8%AE%BF%E9%97%AE%E8%80%85%E6%A8%A1%E5%BC%8F%E3%80%8B.md">189.精读《设计模式 - Visitor 访问者模式》</a>
|
||||
|
||||
### 编译原理
|
||||
|
||||
- <a href="./编译原理/64.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%89%8B%E5%86%99%20SQL%20%E7%BC%96%E8%AF%91%E5%99%A8%20-%20%E8%AF%8D%E6%B3%95%E5%88%86%E6%9E%90%E3%80%8B.md">64.精读《手写 SQL 编译器 - 词法分析》</a>
|
||||
- <a href="./编译原理/65.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%89%8B%E5%86%99%20SQL%20%E7%BC%96%E8%AF%91%E5%99%A8%20-%20%E6%96%87%E6%B3%95%E4%BB%8B%E7%BB%8D%E3%80%8B.md">65.精读《手写 SQL 编译器 - 文法介绍》</a>
|
||||
- <a href="./编译原理/66.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%89%8B%E5%86%99%20SQL%20%E7%BC%96%E8%AF%91%E5%99%A8%20-%20%E8%AF%AD%E6%B3%95%E5%88%86%E6%9E%90%E3%80%8B.md">66.精读《手写 SQL 编译器 - 语法分析》</a>
|
||||
- <a href="./编译原理/67.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%89%8B%E5%86%99%20SQL%20%E7%BC%96%E8%AF%91%E5%99%A8%20-%20%E5%9B%9E%E6%BA%AF%E3%80%8B.md">67.精读《手写 SQL 编译器 - 回溯》</a>
|
||||
- <a href="./编译原理/70.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%89%8B%E5%86%99%20SQL%20%E7%BC%96%E8%AF%91%E5%99%A8%20-%20%E8%AF%AD%E6%B3%95%E6%A0%91%E3%80%8B.md">70.精读《手写 SQL 编译器 - 语法树》</a>
|
||||
- <a href="./编译原理/71.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%89%8B%E5%86%99%20SQL%20%E7%BC%96%E8%AF%91%E5%99%A8%20-%20%E9%94%99%E8%AF%AF%E6%8F%90%E7%A4%BA%E3%80%8B.md">71.精读《手写 SQL 编译器 - 错误提示》</a>
|
||||
- <a href="./编译原理/78.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%89%8B%E5%86%99%20SQL%20%E7%BC%96%E8%AF%91%E5%99%A8%20-%20%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96%E4%B9%8B%E7%BC%93%E5%AD%98%E3%80%8B.md">78.精读《手写 SQL 编译器 - 性能优化之缓存》</a>
|
||||
- <a href="./编译原理/85.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%89%8B%E5%86%99%20SQL%20%E7%BC%96%E8%AF%91%E5%99%A8%20-%20%E6%99%BA%E8%83%BD%E6%8F%90%E7%A4%BA%E3%80%8B.md">85.精读《手写 SQL 编译器 - 智能提示》</a>
|
||||
|
||||
### 源码解读
|
||||
|
||||
- <a href="./源码解读/48.%E7%B2%BE%E8%AF%BB%E3%80%8AImmer.js%E3%80%8B%E6%BA%90%E7%A0%81.md">48.精读《Immer.js》源码</a>
|
||||
- <a href="./源码解读/73.%E7%B2%BE%E8%AF%BB%E3%80%8Asqorn%20%E6%BA%90%E7%A0%81%E3%80%8B.md">73.精读《sqorn 源码》</a>
|
||||
- <a href="./源码解读/75.%E7%B2%BE%E8%AF%BB%E3%80%8AEpitath%20%E6%BA%90%E7%A0%81%20-%20renderProps%20%E6%96%B0%E7%94%A8%E6%B3%95%E3%80%8B.md">75.精读《Epitath 源码 - renderProps 新用法》</a>
|
||||
- <a href="./源码解读/82.%E7%B2%BE%E8%AF%BB%E3%80%8AHtm%20-%20Hyperscript%20%E6%BA%90%E7%A0%81%E3%80%8B.md">82.精读《Htm - Hyperscript 源码》</a>
|
||||
- <a href="./源码解读/92.%E7%B2%BE%E8%AF%BB%E3%80%8AReact%20PowerPlug%20%E6%BA%90%E7%A0%81%E3%80%8B.md">92.精读《React PowerPlug 源码》</a>
|
||||
- <a href="./源码解读/93.%E7%B2%BE%E8%AF%BB%E3%80%8Asyntax-parser%20%E6%BA%90%E7%A0%81%E3%80%8B.md">93.精读《syntax-parser 源码》</a>
|
||||
- <a href="./源码解读/98.%E7%B2%BE%E8%AF%BB%E3%80%8Areact-easy-state%20%E6%BA%90%E7%A0%81%E3%80%8B.md">98.精读《react-easy-state 源码》</a>
|
||||
- <a href="./源码解读/110.%E7%B2%BE%E8%AF%BB%E3%80%8AInject%20Instance%20%E6%BA%90%E7%A0%81%E3%80%8B.md">110.精读《Inject Instance 源码》</a>
|
||||
- <a href="./源码解读/122.%E7%B2%BE%E8%AF%BB%E3%80%8Arobot%20%E6%BA%90%E7%A0%81%20-%20%E6%9C%89%E9%99%90%E7%8A%B6%E6%80%81%E6%9C%BA%E3%80%8B.md">122.精读《robot 源码 - 有限状态机》</a>
|
||||
- <a href="./源码解读/128.%E7%B2%BE%E8%AF%BB%E3%80%8AHooks%20%E5%8F%96%E6%95%B0%20-%20swr%20%E6%BA%90%E7%A0%81%E3%80%8B.md">128.精读《Hooks 取数 - swr 源码》</a>
|
||||
- <a href="./源码解读/130.%E7%B2%BE%E8%AF%BB%E3%80%8Aunstated%20%E4%B8%8E%20unstated-next%20%E6%BA%90%E7%A0%81%E3%80%8B.md">130.精读《unstated 与 unstated-next 源码》</a>
|
||||
- <a href="./源码解读/151.%20%E7%B2%BE%E8%AF%BB%E3%80%8A%40umijs%20use-request%E3%80%8B%E6%BA%90%E7%A0%81.md">151. 精读《@umijs use-request》源码</a>
|
||||
- <a href="./源码解读/155.%20%E7%B2%BE%E8%AF%BB%E3%80%8Ause-what-changed%20%E6%BA%90%E7%A0%81%E3%80%8B.md">155. 精读《use-what-changed 源码》</a>
|
||||
- <a href="./源码解读/156.%20%E7%B2%BE%E8%AF%BB%E3%80%8Areact-intersection-observer%20%E6%BA%90%E7%A0%81%E3%80%8B.md">156. 精读《react-intersection-observer 源码》</a>
|
||||
|
||||
### 商业思考
|
||||
|
||||
- <a href="./商业思考/90.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%9E%81%E5%AE%A2%E5%85%AC%E5%9B%AD%202019%E3%80%8B.md">90.精读《极客公园 2019》</a>
|
||||
- <a href="./商业思考/103.%E7%B2%BE%E8%AF%BB%E3%80%8A%E4%B8%BA%E4%BB%80%E4%B9%88%E4%B8%93%E5%AE%B6%E4%B8%8D%E5%86%8D%E5%85%B3%E5%BF%83%E6%8A%80%E6%9C%AF%E7%BB%86%E8%8A%82%E3%80%8B.md">103.精读《为什么专家不再关心技术细节》</a>
|
||||
- <a href="./商业思考/106.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%95%B0%E6%8D%AE%E4%B9%8B%E4%B8%8A%C2%B7%E6%99%BA%E6%85%A7%E4%B9%8B%E5%85%89%20-%202018%E3%80%8B.md">106.精读《数据之上·智慧之光 - 2018》</a>
|
||||
- <a href="./商业思考/108.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%99%BA%E8%83%BD%E5%95%86%E4%B8%9A%E3%80%8B.md">108.精读《智能商业》</a>
|
||||
- <a href="./商业思考/114.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%B0%81%E5%9C%A8%E4%B8%96%E7%95%8C%E4%B8%AD%E5%BF%83%E3%80%8B.md">114.精读《谁在世界中心》</a>
|
||||
- <a href="./商业思考/115.%E7%B2%BE%E8%AF%BB%E3%80%8ATableau%20%E5%85%A5%E9%97%A8%E3%80%8B.md">115.精读《Tableau 入门》</a>
|
||||
- <a href="./商业思考/116.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%88%B7%E6%96%B0%E3%80%8B.md">116.精读《刷新》</a>
|
||||
- <a href="./商业思考/131.%E7%B2%BE%E8%AF%BB%E3%80%8A%E4%BB%8E%200%20%E5%88%B0%201%E3%80%8B.md">131.精读《从 0 到 1》</a>
|
||||
- <a href="./商业思考/135.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%9E%81%E5%AE%A2%E5%85%AC%E5%9B%AD%20IFX%20-%20%E4%B8%8A%E3%80%8B.md">135.精读《极客公园 IFX - 上》</a>
|
||||
- <a href="./商业思考/136.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%9E%81%E5%AE%A2%E5%85%AC%E5%9B%AD%20IFX%20-%20%E4%B8%8B%E3%80%8B.md">136.精读《极客公园 IFX - 下》</a>
|
||||
- <a href="./商业思考/137.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%BD%93%E6%88%91%E5%9C%A8%E5%88%86%E4%BA%AB%E7%9A%84%E6%97%B6%E5%80%99%EF%BC%8C%E6%88%91%E5%9C%A8%E5%81%9A%E4%BB%80%E4%B9%88%EF%BC%9F%E3%80%8B.md">137.精读《当我在分享的时候,我在做什么?》</a>
|
||||
|
||||
### 算法
|
||||
|
||||
- <a href="./算法/198.%E7%B2%BE%E8%AF%BB%E3%80%8A%E7%AE%97%E6%B3%95%20-%20%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E3%80%8B.md">198.精读《算法 - 动态规划》</a>
|
||||
- <a href="./算法/199.%E7%B2%BE%E8%AF%BB%E3%80%8A%E7%AE%97%E6%B3%95%20-%20%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E3%80%8B.md">199.精读《算法 - 滑动窗口》</a>
|
||||
- <a href="./算法/200.%E7%B2%BE%E8%AF%BB%E3%80%8A%E7%AE%97%E6%B3%95%20-%20%E5%9B%9E%E6%BA%AF%E3%80%8B.md">200.精读《算法 - 回溯》</a>
|
||||
- <a href="./算法/201.%E7%B2%BE%E8%AF%BB%E3%80%8A%E7%AE%97%E6%B3%95%20-%20%E4%BA%8C%E5%8F%89%E6%A0%91%E3%80%8B.md">201.精读《算法 - 二叉树》</a>
|
||||
- <a href="./算法/203.%E7%B2%BE%E8%AF%BB%E3%80%8A%E7%AE%97%E6%B3%95%20-%20%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91%E3%80%8B.md">203.精读《算法 - 二叉搜索树》</a>
|
||||
|
||||
## 关注前端精读微信公众号
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
|
||||
|
||||
## Special Sponsors
|
||||
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://e.coding.net/?utm_source=weekly" target="_blank">
|
||||
<img width="300" src="https://img.alicdn.com/tfs/TB107D.QbrpK1RjSZTEXXcWAVXa-1000-332.png">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -8,9 +8,9 @@
|
||||
|
||||
# 1 引言
|
||||
|
||||
<img src="assets/1/cube.jpeg" alt="logo" width="500" />
|
||||
<img src="https://img.alicdn.com/imgextra/i4/O1CN01mvDKCM1owPSsLDBmI_!!6000000005289-2-tps-475-297.png" alt="logo" width="500" />
|
||||
|
||||
> 如今,Javascript 模块化规范非常方便、自然,但这个新规范仅执行了2年,就在 4 年前,js 的模块化还停留在运行时支持,10 年前,通过后端模版定义、注释定义模块依赖。对经历过来的人来说,历史的模块化方式还停留在脑海中,反而新上手的同学会更快接受现代的模块化规范。
|
||||
> 如今,Javascript 模块化规范非常方便、自然,但这个新规范仅执行了 2 年,就在 4 年前,js 的模块化还停留在运行时支持,10 年前,通过后端模版定义、注释定义模块依赖。对经历过来的人来说,历史的模块化方式还停留在脑海中,反而新上手的同学会更快接受现代的模块化规范。
|
||||
|
||||
但为什么要了解 Javascript 模块化发展的历史呢?因为凡事都有两面性,了解 Javascript 模块化规范,有利于我们思考出更好的模块化方案,纵观历史,从 1999 年开始,模块化方案最多维持两年,就出现了新的替代方案,比原有的模块化更清晰、强壮,我们不能被现代模块化方式限制住思维,因为现在的 ES2015 模块化方案距离发布也仅仅过了两年。
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
|
||||
**外部依赖定义 (2007)**: 这种定义方式在 cocos2d-js 开发中普遍使用,其核心思想是将依赖抽出单独文件定义,这种方式不利于项目管理,毕竟依赖抽到代码之外,我是不是得两头找呢?所以才有通过 webpack 打包为一个文件的方式暴力替换为 commonjs 的方式出现。
|
||||
|
||||
**Sandbox模式 (2009)**: 这种模块化方式很简单,暴力,将所有模块塞到一个 `sanbox` 变量中,硬伤是无法解决明明冲突问题,毕竟都塞到一个 `sandbox` 对象里,而 `Sandbox` 对象也需要定义在全局,存在被覆盖的风险。模块化需要保证全局变量尽量干净,目前为止的模块化方案都没有很好的做到这一点。
|
||||
**Sandbox 模式 (2009)**: 这种模块化方式很简单,暴力,将所有模块塞到一个 `sandbox` 变量中,硬伤是无法解决命名冲突问题,毕竟都塞到一个 `sandbox` 对象里,而 `Sandbox` 对象也需要定义在全局,存在被覆盖的风险。模块化需要保证全局变量尽量干净,目前为止的模块化方案都没有很好的做到这一点。
|
||||
|
||||
**依赖注入 (2009)**: 就是大家熟知的 angular1.0,依赖注入的思想现在已广泛运用在 react、vue 等流行框架中。但依赖注入和解决模块化问题还差得远。
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
|
||||
这篇文章所提供的模块化历史的方案都是逻辑模块化,**从 CommonJS 方案开始前端把服务端的解决方案搬过来之后,算是看到标准物理与逻辑统一的模块化**。但之后前端工程不得不引入模块化构建这一步。正是这一步给前端开发无疑带来了诸多的不便,尤其是现在我们开发过程中经常为了优化这个工具带了很多额外的成本。
|
||||
|
||||
从 CommonJS 之前其实都只是封装,并没有一套模块化规范,这个就有些像类与包的概念。我在10年左右用的最多的还是 YUI2,YUI2 是用 namespace 来做模块化的,但有很多问题没有解决,比如多版本共存,因此后来 YUI3 出来了。
|
||||
从 CommonJS 之前其实都只是封装,并没有一套模块化规范,这个就有些像类与包的概念。我在 10 年左右用的最多的还是 YUI2,YUI2 是用 namespace 来做模块化的,但有很多问题没有解决,比如多版本共存,因此后来 YUI3 出来了。
|
||||
|
||||
```javascript
|
||||
YUI().use('node', 'event', function (Y) {
|
||||
@@ -103,7 +103,7 @@ YUI3 的 sandbox 像极了差不多同时出现的 AMD 规范,但早期 yahoo
|
||||
|
||||
> 看到大家基本都提到了 HTTP/2,对这项技术解决前端模块化及资源打包等工程问题抱有非常大的期待。很多人也认为 HTTP/2 普及后,基本就没有 Webpack 什么事情了。
|
||||
|
||||
不过 Webpack 作者 @sokra 在他的文章 [webpack & HTTP/2](https://medium.com/webpack/webpack-http-2-7083ec3f3ce6#.zdo4juvgo) 里提到了一个新的 Webpack 插件 `AggressiveSplittingPlugin`。简单的说,这款插件就是为了充分利用 HTTP/2 的文件缓存能力,将你的业务代码自动拆分成若干个数十 KB 的小文件。后续若其中任意一个文件发生变化,可以保证其他的小 chunck 不需要重新下载。
|
||||
不过 Webpack 作者 @sokra 在他的文章 [webpack & HTTP/2](https://medium.com/webpack/webpack-http-2-7083ec3f3ce6#.zdo4juvgo) 里提到了一个新的 Webpack 插件 `AggressiveSplittingPlugin`。简单的说,这款插件就是为了充分利用 HTTP/2 的文件缓存能力,将你的业务代码自动拆分成若干个数十 KB 的小文件。后续若其中任意一个文件发生变化,可以保证其他的小 chunk 不需要重新下载。
|
||||
|
||||
可见,**即使不断的有新技术出现,也依然需要配套的工具来将前端工程问题解决方案推向极致。**
|
||||
|
||||
@@ -118,7 +118,7 @@ YUI3 的 sandbox 像极了差不多同时出现的 AMD 规范,但早期 yahoo
|
||||
### 补充阅读
|
||||
|
||||
- [JavaScript 模块化七日谈](https://huangxuan.me/2015/07/09/js-module-7day/)
|
||||
- [JavaScript模块化编程简史(2009-2016)](https://yuguo.us/weblog/javascript-module-development-history/)
|
||||
- [JavaScript 模块化编程简史(2009-2016)](https://yuguo.us/weblog/javascript-module-development-history/)
|
||||
|
||||
# 总结
|
||||
|
||||
@@ -136,4 +136,4 @@ YUI3 的 sandbox 像极了差不多同时出现的 AMD 规范,但早期 yahoo
|
||||
|
||||
至此,对于 javascript 模块化讨论已接近尾声,对其优缺点也基本达成了一致。前端复杂度不断提高,促使着模块化的改进,代理(浏览器、node) 的支持程度,与前端特殊性(流量、缓存)可能前端永远也离不开构建工具,新的标准会让这些工作做的更好,同时取代、增强部分特征,前端的未来是更加美好的,复杂度也更高。
|
||||
|
||||
**如果你想参与讨论,请[点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,每周五发布。**
|
||||
**如果你想参与讨论,请[点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,每周五发布。**
|
||||
@@ -7,29 +7,29 @@
|
||||
|
||||
我为什么要选这篇文章呢?
|
||||
|
||||
就在前几天的 Google I/O 2017上, Polymer 正式发布了 [Polymer 2.0](https://www.polymer-project.org/blog/2017-05-15-time-for-two) 版本.
|
||||
就在前几天的 Google I/O 2017 上, Polymer 正式发布了 [Polymer 2.0](https://www.polymer-project.org/blog/2017-05-15-time-for-two) 版本.
|
||||
|
||||
来看一下 Polymer 2.0 的一些变化:
|
||||
- 使用 Shadow DOM v1 代替 Polymer.dom. Shady DOM从 Polymer 中分离出来。
|
||||
- 使用 Shadow DOM v1 代替 Polymer.dom. Shady DOM 从 Polymer 中分离出来。
|
||||
- 使用 标准的 ES6 类和 Custom Elements v1 来自定义元素.
|
||||
- 还有数据系统的改进和生命周期的变更.
|
||||
|
||||
可以看到, Polymer 的这次升级主要是将 Shadow Dom 和 Custom Elements 升级到 v1 版本, 以获得更多浏览器的原生支持. 下一 代Web Components - v1规范,Chrome 已经支持了,Web Components 规范中的2个主要部分 - [Shadow Dom](https://www.chromestatus.com/feature/4667415417847808) 和 [Custom Elements](https://www.chromestatus.com/feature/4696261944934400). Safari在10版本中, 支持了 [Shadow DOM v1](https://webkit.org/status/#feature-shadow-dom) 规范并且完成了在Webkit内核中对 [Custom Elements v1](https://webkit.org/blog/7027/introducing-custom-elements/) 规范的实现;Firefox对 [Shadow DOM](https://platform-status.mozilla.org/#shadow-dom) 和 [Custom Elements v1规范](https://platform-status.mozilla.org/#custom-elements) 支持正在开发中;Edge也将对 [Shadow DOM](https://developer.microsoft.com/en-us/microsoft-edge/platform/status/shadowdom/) 和 [Custom Elements](https://developer.microsoft.com/en-us/microsoft-edge/platform/status/customelements/) 支持规划到他们的开发roadmap中。
|
||||
可以看到, Polymer 的这次升级主要是将 Shadow Dom 和 Custom Elements 升级到 v1 版本, 以获得更多浏览器的原生支持. 下一 代 Web Components - v1 规范,Chrome 已经支持了,Web Components 规范中的 2 个主要部分 - [Shadow Dom](https://www.chromestatus.com/feature/4667415417847808) 和 [Custom Elements](https://www.chromestatus.com/feature/4696261944934400). Safari 在 10 版本中, 支持了 [Shadow DOM v1](https://webkit.org/status/#feature-shadow-dom) 规范并且完成了在 Webkit 内核中对 [Custom Elements v1](https://webkit.org/blog/7027/introducing-custom-elements/) 规范的实现;Firefox 对 [Shadow DOM](https://platform-status.mozilla.org/#shadow-dom) 和 [Custom Elements v1 规范](https://platform-status.mozilla.org/#custom-elements) 支持正在开发中;Edge 也将对 [Shadow DOM](https://developer.microsoft.com/en-us/microsoft-edge/platform/status/shadowdom/) 和 [Custom Elements](https://developer.microsoft.com/en-us/microsoft-edge/platform/status/customelements/) 支持规划到他们的开发 roadmap 中。
|
||||
|
||||
这段时间, 大家都在讨论 react, vue, angular, 这些框架. 或者 该使用 redux 还 是 mobx 做数据管理. 在这个契机下, 我想我们可以不单单去思考这些框架, 也可以更多地去思考和了解 Web Components 标准. 对于 Web Components标准有一些思考. 所以我选了一篇关于 Web Components 的文章, 想让大家对于 Web Components 的发展, 和 Web Componets 与现在的主流框架如何协作有更多的思考和讨论.
|
||||
这段时间, 大家都在讨论 react, vue, angular, 这些框架. 或者 该使用 redux 还 是 mobx 做数据管理. 在这个契机下, 我想我们可以不单单去思考这些框架, 也可以更多地去思考和了解 Web Components 标准. 对于 Web Components 标准有一些思考. 所以我选了一篇关于 Web Components 的文章, 想让大家对于 Web Components 的发展, 和 Web Componets 与现在的主流框架如何协作有更多的思考和讨论.
|
||||
|
||||
|
||||
# 2 内容概要
|
||||
|
||||
**The broken promise of Web Components**
|
||||
原文作者dmitriid主要是在喷Web Components从2011年到2017年这6年间毫无进展, 一共产出了6份标准, 其中两份已经被弃用. 几乎只有一个主流浏览器(chrome) 支持.
|
||||
原文作者 dmitriid 主要是在喷 Web Components 从 2011 年到 2017 年这 6 年间毫无进展, 一共产出了 6 份标准, 其中两份已经被弃用. 几乎只有一个主流浏览器(chrome) 支持.
|
||||
|
||||

|
||||
|
||||
|
||||
- Web Components 这些规范强依赖 JS 的实现
|
||||
- Custom Elements 是 JS 脚本的一部分
|
||||
- HTML Templates 的出现就是为了被JS 脚本使用
|
||||
- HTML Templates 的出现就是为了被 JS 脚本使用
|
||||
- Shadow Dom 也需要配合 JS 脚本使用
|
||||
- 只有 HTML imports 可以脱离 JS 脚本使用
|
||||
- Web Components 操作 DOM
|
||||
@@ -38,16 +38,16 @@
|
||||
- 为了突破限制使用不同的方法来传递数据
|
||||
- CSS 作用域, 可以见上次精读[《请停止 css-in-js 的行为》](https://github.com/dt-fe/weekly/issues/12)
|
||||
|
||||
**来看一下Polymer 的 核心成员 Rob Dodson 对于本文的回应: Regarding the broken promise of Web Components**
|
||||
**来看一下 Polymer 的 核心成员 Rob Dodson 对于本文的回应: Regarding the broken promise of Web Components**
|
||||
|
||||
- Web Components 特性需要被浏览器支持,必须有平缓的过渡,良好的兼容,以及成熟的方案,因此推进速度会比较慢一些。
|
||||
- React 很棒, 但是也不要忽略其他基于 Web Components 的优秀库比如 [Amp](https://www.ampproject.org/)
|
||||
- 对于 DOM 更新的抽象比如 React/JSX很赞, 但是也带来了一些损耗. 在旧的移动设备上, 加载一个大的js 包性能依旧不理想, 最佳的做法是拆分你的 JS 包, 按需加载.
|
||||
- 使用 JSX 和 虚拟 DOM是很酷, 也可以直接把 JSX 用在 Web Components 内, 像[SkateJS](https://github.com/skatejs/skatejs)库, 已经在做这个事情了.
|
||||
- 没有标准的数据绑定, Polymer的数据绑定, 现在是基于[MDV](https://github.com/toolkitchen/mdv), 很多开发者更倾向于基于 Observables或者 ES6 Proxies的数据绑定方案.
|
||||
- 处理组件的字符串属性是很烦人, 但是由于每一个组件都是一个类的实例, 可以利用ES6 的 getters/setters来改变属性.
|
||||
- 对于 DOM 更新的抽象比如 React/JSX 很赞, 但是也带来了一些损耗. 在旧的移动设备上, 加载一个大的 js 包性能依旧不理想, 最佳的做法是拆分你的 JS 包, 按需加载.
|
||||
- 使用 JSX 和 虚拟 DOM 是很酷, 也可以直接把 JSX 用在 Web Components 内, 像[SkateJS](https://github.com/skatejs/skatejs)库, 已经在做这个事情了.
|
||||
- 没有标准的数据绑定, Polymer 的数据绑定, 现在是基于[MDV](https://github.com/toolkitchen/mdv), 很多开发者更倾向于基于 Observables 或者 ES6 Proxies 的数据绑定方案.
|
||||
- 处理组件的字符串属性是很烦人, 但是由于每一个组件都是一个类的实例, 可以利用 ES6 的 getters/setters 来改变属性.
|
||||
|
||||
Rob Dodson对于 Web Components 依然充满信心, 但是也承认推进标准总会有各种阻碍, 不可能像推荐框架一样快速把事情解决.
|
||||
Rob Dodson 对于 Web Components 依然充满信心, 但是也承认推进标准总会有各种阻碍, 不可能像推荐框架一样快速把事情解决.
|
||||
|
||||
# 3 精读
|
||||
|
||||
@@ -55,11 +55,11 @@ Rob Dodson对于 Web Components 依然充满信心, 但是也承认推进标准
|
||||
[@camsong](https://www.zhihu.com/people/078cc0fb15845759ad8295b0f0e50099) [@黄子毅](https://github.com/ascoders) [@杨森](https://www.zhihu.com/people/c93b7957f6308990c7e3b16103c9356b) [@rccoder](https://github.com/rccoder) [@alcat2008](https://github.com/alcat2008)精读由此归纳。
|
||||
|
||||
### 标准与框架
|
||||
Web Components 作为一个标准,骨子里的进度就会落后于当前可行的技术体系。正如文中所说,浏览器厂商 ship 一个新功能是很严肃的,很可能会影响到一票的线上业务,甚至会影响到一个产业(遥想当年 [Chrome Extension 禁用 NPAPI](https://blog.chromium.org/2013/09/saying-goodbye-to-our-old-friend-npapi.html)时的一片哀鸿遍野,许多返利插件都使用了这种技术)。那么 Web Components的缓慢推进也在情理之中了.
|
||||
即使真的有一天这个标准建立起来,Web Components作为浏览器底层特性不应该拿出来和React这类应用层框架相比较. 未来Web Components会做为浏览器非常重要的特性存在。API偏低层操作,会易用性不够. 在很长时间内开发者依旧会使用 React/Vue/Angular/Polymer 这样的框架,Web Components可能会做为这些框架的底层做一些 浏览器层面上的支持.
|
||||
Web Components 作为一个标准,骨子里的进度就会落后于当前可行的技术体系。正如文中所说,浏览器厂商 ship 一个新功能是很严肃的,很可能会影响到一票的线上业务,甚至会影响到一个产业(遥想当年 [Chrome Extension 禁用 NPAPI](https://blog.chromium.org/2013/09/saying-goodbye-to-our-old-friend-npapi.html)时的一片哀鸿遍野,许多返利插件都使用了这种技术)。那么 Web Components 的缓慢推进也在情理之中了.
|
||||
即使真的有一天这个标准建立起来,Web Components 作为浏览器底层特性不应该拿出来和 React 这类应用层框架相比较. 未来 Web Components 会做为浏览器非常重要的特性存在。API 偏低层操作,会易用性不够. 在很长时间内开发者依旧会使用 React/Vue/Angular/Polymer 这样的框架,Web Components 可能会做为这些框架的底层做一些 浏览器层面上的支持.
|
||||
|
||||
### 不需要 vendor 的自定义组件间调用
|
||||
在 Webpack 大行其道的时代,想在运行时做到组件即引即用变得很困难,因为这些组件大多是通过 React/Vue/Angular 开发的。不得不考虑引入一大堆 Vendor 包,这些 Vendor 里可能还必须包含 React 这类两个版本不能同时使用的库。目前我们团队在做组件化方案时就遇到这个问题,只能想办法避免两个版本的出现。你可以说这是 React 或 Webpack 引入的问题,但并没有看到 Web Compnents 标准化的解决方案。我想未来Web Components可能会作为浏览器的底层, 出现基于底层的标准方案来做组件间的相互应用的方法.
|
||||
在 Webpack 大行其道的时代,想在运行时做到组件即引即用变得很困难,因为这些组件大多是通过 React/Vue/Angular 开发的。不得不考虑引入一大堆 Vendor 包,这些 Vendor 里可能还必须包含 React 这类两个版本不能同时使用的库。目前我们团队在做组件化方案时就遇到这个问题,只能想办法避免两个版本的出现。你可以说这是 React 或 Webpack 引入的问题,但并没有看到 Web Compnents 标准化的解决方案。我想未来 Web Components 可能会作为浏览器的底层, 出现基于底层的标准方案来做组件间的相互应用的方法.
|
||||
|
||||
|
||||
### 为什么对 Web components 讨论不断
|
||||
@@ -69,7 +69,7 @@ Web Components 作为一个标准,骨子里的进度就会落后于当前可
|
||||
但使用前端框架的问题也日益暴露,随着前端框架种类的增多,同一个框架不同版本之间无法共存,导致组件无法跨框架复用,甚至只能固定在框架的某个版本,这与前端未来的模块化发展是相违背的,我们越是与之抗衡,就越希望 Web components 能站出来解决这个问题,因为浏览器原生支持模块化,相当于将 react angular vue 的能力内置在浏览器中,而且一定会向前兼容(这也是 Web components 推进缓慢的原因)。
|
||||
|
||||
# 4 总结
|
||||
我觉得 Web Components作为浏览器底层特性不应该拿出来和React, vue 这类应用层框架相比较. Web Components 的方向以及提供的价值都不会跟 应用框架一致. 而 Web Components 作为未来的 Web 组件标准 , 它在任何生态中都可以运行良好. 我倒是更加期待应用层去基于 Web Components 去做更多的实现, 让组件超越框架存在, 可以在不同技术栈中使用.
|
||||
我觉得 Web Components 作为浏览器底层特性不应该拿出来和 React, vue 这类应用层框架相比较. Web Components 的方向以及提供的价值都不会跟 应用框架一致. 而 Web Components 作为未来的 Web 组件标准 , 它在任何生态中都可以运行良好. 我倒是更加期待应用层去基于 Web Components 去做更多的实现, 让组件超越框架存在, 可以在不同技术栈中使用.
|
||||
|
||||
|
||||
> 讨论地址是:[精读《Web Components 的困境》 · Issue #15 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/15)
|
||||
@@ -71,7 +71,7 @@ function Counter() {
|
||||
|
||||
如果我们 **在三秒内连续点击三次**,那么 `count` 的值最终会变成 `3`,而随之而来的输出结果是。。?
|
||||
|
||||
```
|
||||
```plain
|
||||
0
|
||||
1
|
||||
2
|
||||
@@ -109,7 +109,7 @@ class Counter extends Component {
|
||||
|
||||
嗯,结果应该等价吧?3 秒内快速点击三次按钮,这次的结果是:
|
||||
|
||||
```
|
||||
```plain
|
||||
3
|
||||
3
|
||||
3
|
||||
@@ -505,7 +505,7 @@ function Counter() {
|
||||
|
||||
我们将 `count` 作为了 `useEffect` 的依赖项,就得到了正确的结果:
|
||||
|
||||
```
|
||||
```plain
|
||||
1
|
||||
2
|
||||
3
|
||||
@@ -813,25 +813,30 @@ function Parent() {
|
||||
换一个例子就可以看得更清楚:
|
||||
|
||||
```js
|
||||
function Parent() {
|
||||
function Parent(props) {
|
||||
const [count, setCount] = useState(0);
|
||||
const [step, setStep] = useState(0);
|
||||
const [other, setOther] = useState(0);
|
||||
const drag = useDraggable(count, step); // 封装了拖拽函数
|
||||
const drag = useDraggable(props.dom, count, step); // 封装了拖拽函数
|
||||
|
||||
useEffect(() => {
|
||||
// dom 变化时重新实例化
|
||||
drag()
|
||||
}, [drag])
|
||||
}
|
||||
```
|
||||
|
||||
假设我们使用 [Sortablejs](https://github.com/SortableJS/Sortable) 对某个区域进行拖拽监听,这个函数每次都重复执行的性能损耗非常大,**然而这个函数内部可能因为仅仅要上报一些日志,所以依赖了没有实际被使用的 `count` `step` 变量:**
|
||||
|
||||
```js
|
||||
function useDraggable(count, step) {
|
||||
function useDraggable(dom, count, step) {
|
||||
return useCallback(() => {
|
||||
// 上报日志
|
||||
report(count, step);
|
||||
|
||||
// 对区域进行初始化,非常耗时
|
||||
// ... 省略耗时代码
|
||||
}, [count, step]);
|
||||
}, [dom, count, step]);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -964,7 +969,7 @@ function Parent() {
|
||||
}
|
||||
```
|
||||
|
||||
虽然 `Child` 可以通过 `memo` 或 `useMemo` 进行优化,**但当程序复杂时,可能存在多个函数在所有 Function Component 间共享的情况 **,此时就需要新 Hook: `useContext` 来拯救了。
|
||||
虽然 `Child` 可以通过 `memo` 或 `useMemo` 进行优化,**但当程序复杂时,可能存在多个函数在所有 Function Component 间共享的情况**,此时就需要新 Hook: `useContext` 来拯救了。
|
||||
|
||||
### 使用 Context 做批量透传
|
||||
|
||||
@@ -1130,7 +1135,7 @@ const Step = () => {
|
||||
一个普通的 Redux 组件:
|
||||
|
||||
```js
|
||||
const mapStateToProps = state => (count: state.count);
|
||||
const mapStateToProps = state => ({count: state.count});
|
||||
|
||||
const mapDispatchToProps = dispatch => dispatch;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
# 1 引言
|
||||
|
||||
<img src="assets/11/coffee.jpg" width="500" alt="logo" />
|
||||
<img src="https://img.alicdn.com/imgextra/i2/O1CN01JC1TZ51Nxn24teojP_!!6000000001637-2-tps-1024-1296.png" width="500" alt="logo" />
|
||||
|
||||
梵高这幅画远景漆黑一片,近景的咖啡店色彩却反差很大,他只是望着黑夜中温暖的咖啡馆,交织着矛盾与孤独。代码不可能没有 BUG,调试与开发也始终交织在一起,我们在这两种矛盾中不断成长。
|
||||
|
||||
@@ -62,7 +62,7 @@ Chrome Dev Tools 非常强大,[dev-tips](https://umaar.com/dev-tips/) 列出
|
||||
|
||||
### 移动端控制台
|
||||
|
||||
- [Chrome远程调试](https://developers.google.com/web/tools/chrome-devtools/remote-debugging/webviews) app 支持后,连接 usb 或者局域网,即可通过 Dev Tools 调试 webview 页面。
|
||||
- [Chrome 远程调试](https://developers.google.com/web/tools/chrome-devtools/remote-debugging/webviews) app 支持后,连接 usb 或者局域网,即可通过 Dev Tools 调试 webview 页面。
|
||||
- [Weinre](http://people.apache.org/~pmuellr/weinre/docs/latest/Home.html) 通过页面加载脚本,与 pc 端调试器通信。
|
||||
- 通过内嵌控制台解决,比如 [eruda](http://eruda.liriliri.io/) [VConsole](https://github.com/WechatFE/vConsole)
|
||||
- [Rosin](http://alloyteam.github.io/Rosin/) fiddler 的一个插件,协助移动页面调试。
|
||||
@@ -105,7 +105,7 @@ Css 不像 Js 一样方便分析规则是否存在冗余,Chrome 帮我们做
|
||||
|
||||
Chrome 会记录最后插入的 5 个元素,分别以 `$0` ~ `$4` 的方式在控制台直接输出。
|
||||
|
||||
<img src="assets/11/last-item.png" width="500" alt="last-items" />
|
||||
<img src="https://img.alicdn.com/imgextra/i4/O1CN01xsKHb822gkXhDDcCA_!!6000000007150-2-tps-417-516.png" width="500" alt="last-items" />
|
||||
|
||||
### Console.table
|
||||
|
||||
@@ -140,7 +140,7 @@ const App = epitath(function*() {
|
||||
|
||||
其核心是利用 `generator` 的迭代,将 React 组件的平级结构还原成嵌套结构,将嵌套写法打平了:
|
||||
|
||||
```
|
||||
```plain
|
||||
yield <A>
|
||||
yield <B>
|
||||
yield <C>
|
||||
@@ -0,0 +1,541 @@
|
||||
# 1. 引言
|
||||
|
||||
Tableau 探索式分析功能非常强大,各种功能组合似乎有着无限的可能性。
|
||||
|
||||
今天笔者会分析这种探索式模型解题思路,一起看看这种探索式分析功能是如何做到的。
|
||||
|
||||
# 2. 精读
|
||||
|
||||
要掌握探索式分析,先要掌握探索式分析背后的思维模型。
|
||||
|
||||
## 理解数据
|
||||
|
||||
有分析意义的数据一般是表结构,即分为行与列,列定义了数据含义,行则构成了数据明细。
|
||||
|
||||
当我们将数据作为 “原材料” 使用时,需要将这些明细数据封装为 “数据集” 的概念来理解,数据集概念中,数据就是一个个字段,对于字段,要理解 “维度” 与 “度量” 这两个概念。
|
||||
|
||||
### 维度
|
||||
|
||||
维度是不能被计数的字段,一般为字符串或离散的值,用来描述数据的维度。
|
||||
|
||||
### 度量
|
||||
|
||||
度量是可以被计数的字段,一般为数字、日期等连续的值,用来描述数据的量。
|
||||
|
||||
<img width=172 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566632483137-9e0268d9-f890-45e6-a3e5-805355b35af9.png#align=left&display=inline&height=464&name=image.png&originHeight=1096&originWidth=406&size=83329&status=done&width=172">
|
||||
|
||||
我们首先要将数据集字段归类到维度与度量,才能提高数据分析的效率。**数据分析就是从不同维度下看度量值**,先想清楚要看的是什么数据,比如销量还是利润?这些字段都属于度量,然后想一想要怎么看这些度量,是看总数、拆解到年看、还是按地区看呢?这些字段都属于维度。
|
||||
|
||||
**维度和度量是可以单独看的,如果单看维度,那只能看这个维度的明细,比如看 订单日期 这个字段**:
|
||||
|
||||
<img width=190 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566633158647-cba541bd-673c-498c-95e6-53c89346c458.png#align=left&display=inline&height=149&name=image.png&originHeight=298&originWidth=380&size=24178&status=done&width=190">
|
||||
|
||||
需要注意的时,维度与度量字段还可以分为 **连续** 与 **离散** 。
|
||||
|
||||
### 连续
|
||||
|
||||
值是连续关系,即任意两个值之间可以计算差值。
|
||||
|
||||
### 离散
|
||||
|
||||
值是离散关系,即任意两个值之间无法计算差值,无法以连续的方式去理解。
|
||||
|
||||
**一般来说,维度字段都是离散的,度量字段都是连续的。**从字段类型意义上也能得出相同的结论:维度字段一般为字符串或日期类型,字符串类型都是离散的,度量字段一般为数字类型,数字天生就可以连续。
|
||||
|
||||
值得注意的是,连续与离散其实与字段类型、维度度量并无关系,比如维度的日期字段就是可连续的,而就算是字符串类型,也可以以字符串长度等方式 “定义” 一种连续的计算方式。对数字类型的度量字段来说,我们也可以忽略数字之间的联系,将数字看待为字符串,这样数字之间就是离散的。
|
||||
|
||||
**上图的 “离散方式看日期” 就是看维度的直观方式,但仍可以用 “连续方式看日期”:**
|
||||
|
||||
<img width=309 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566633194083-b17c1c2c-7023-47cd-a94c-48d57fd37217.png#align=left&display=inline&height=308&name=image.png&originHeight=616&originWidth=618&size=37644&status=done&width=309">
|
||||
|
||||
离散方式下单看维度只有一条条数据,数据间并无排序规则,而以连续方式看维度,维度就会以某种方式排序:比如上图以时间类型进行排序。此时展示方式也从表格切换为了柱状图,因为表格适合展示离散数据,柱状图的一根柱子就可以展示连续数据。
|
||||
|
||||
单看度量时,由于 **度量要依附于维度展示**,因此仅有度量时,只能看这个度量的 **聚合** 概念:
|
||||
|
||||
<img width=200 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566633468748-6ffb22b0-8c8d-4f6c-bdb6-a16616e29e70.png#align=left&display=inline&height=107&name=image.png&originHeight=214&originWidth=400&size=15238&status=done&width=200">
|
||||
|
||||
如上图所示,单看销量这个度量字段时,我们只能将数据集中所有销量字段聚合在一起来看,**但这种聚合方式也可以分成若干种计算类型 - 求和、平均值、中位数、计数、计数去重、最小值、最大值、方差等等:**
|
||||
|
||||
<img width=414 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566633611811-f0bef366-b9ca-47dd-adbc-4e2d311f52de.png#align=left&display=inline&height=502&name=image.png&originHeight=1004&originWidth=828&size=228956&status=done&width=414">
|
||||
|
||||
这些能力之间都是 “正交” 的,即单看度量这一个字段,可以以这么多种类型进行计算,那么按维度拆分后,度量依然可以享受如上不同的计算方式。
|
||||
|
||||
**也可以用连续方式看度量:**
|
||||
|
||||
<img width=184 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566633797820-b7980691-1be7-4644-a321-cac98cc36e5f.png#align=left&display=inline&height=304&name=image.png&originHeight=608&originWidth=368&size=23542&status=done&width=184">
|
||||
|
||||
与连续-维度不同,连续-度量图形中除了最后一个值,其他过渡数值都是无效的,因为连续-度量只有一个值。连续-维度也要注意,由于以连续的方式画出图形,中间不存在的点也被 “无缝连接” 了。
|
||||
|
||||
数据之间也可以存在父子级关系,有父子级关系就可以进行上卷下钻了,这种父子级关系被称为 “层系字段”:
|
||||
|
||||
<img width=209 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566634402929-9e23bff4-4810-4827-bf3b-c6af9ef617ea.png#align=left&display=inline&height=217&name=image.png&originHeight=434&originWidth=418&size=33356&status=done&width=209">
|
||||
|
||||
上图的 Orders 就是一个层系字段。层系字段是几个字段的排序组合,**由上到下依次构成下钻关系,从下到上则是上卷的关系。**
|
||||
|
||||
### 层系
|
||||
|
||||
**只有维度字段才能有层系,**因为度量是不能被拆分的,只有维度才可以被拆分。
|
||||
|
||||
维度的拆分可以是有逻辑含义的,也可以是任意的。
|
||||
|
||||
**有逻辑含义的层系**
|
||||
|
||||
最典型有逻辑含义的层系字段就是时间了。一个好的 BI 系统识别到日期字段后,应该将拿到的日期字段进行归类,比如判断日期字段粒度到天,则自动生成一个日期层系字段,自动聚合到年,并允许用户随意切换:
|
||||
|
||||
<img width=277 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566634723539-8f80cdd3-f8af-41a2-9e89-db94ecfa3200.png#align=left&display=inline&height=161&name=image.png&originHeight=322&originWidth=554&size=51469&status=done&width=277">
|
||||
|
||||
如果数据集字段值精确到月,则层系只能最多展开到月。
|
||||
|
||||
日期层系的逻辑含义在于,年、季度、月、天这种下钻关系是天然从大到小的关系,符合自然理解。
|
||||
|
||||
**任意层系**
|
||||
|
||||
如果层系字段不代表日期,就只能以业务含义组合层系字段了。**比如可以将层系按照 订单日期 -> 商品 ID -> 运货日期的方式组合:**
|
||||
|
||||
<img width=624 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566634964577-6dad1f7c-b01b-419a-8b7c-6e0832d6c8b6.png#align=left&display=inline&height=132&name=image.png&originHeight=308&originWidth=1454&size=41917&status=done&width=624">
|
||||
|
||||
这种下钻方式,可以看到每个订单日期下有哪些商品,每个商品分别运货日期是什么。
|
||||
|
||||
**也可以按照商品 ID 拆分出不同的订单日期与运货日期,这种层系组合方式就是以商品 ID 为主要视角:**
|
||||
|
||||
<img width=622 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566635114693-57a2b260-d7cc-4945-9050-361ce4608e09.png#align=left&display=inline&height=129&name=image.png&originHeight=302&originWidth=1456&size=44825&status=done&width=622">
|
||||
|
||||
可以看到,不同思维角度会按照不同的方式组合层系。比如一家大公司要查看财务问题,维度有:BU、日期,度量有:销量。
|
||||
|
||||
那么有两种下钻方式:BU -> 日期、日期 -> BU。无论哪种下钻方式,都能看到每个 BU 按日期销量的明细,但 BU -> 日期 能看到每个 BU 按日期聚合的总销量,而 日期 -> BU 能看到不同日期按 BU 聚合的总销量,前者更易对比出 BU 之间差异,后者更易对比出日期之间的差异。
|
||||
|
||||
## 理解配置
|
||||
|
||||
配置是探索式分析的入口,要理解分析模型首先得理解配置模型。
|
||||
|
||||
Table 主要配置分为行、列、标记与筛选。通过这四个配置区域可以组合成千变万化的数据洞察模型。既然如此,让我们看看这种配置思路是什么,以及为何这四种配置相互组合就能覆盖整个探索式分析场景?
|
||||
|
||||
我们不需要考虑三维数据分析场景,因为三维透视的关系,图形丢失了精确大小关系,没有精度的数据是没有分析价值的。由于在二位平面中分析数据,**大部分图表都可以用 “行、列” 方式进行配置**。
|
||||
|
||||
**也许有人会问,为什么不用维度与度量替代行列呢**?这是一个很好的问题,有数据分析经验的人会站在维度与度量角度思考问题,因此对于任意图表,只要配置维度、度量即可呀?笔者从三个方面说说自己的理解:
|
||||
|
||||
1. 探索式分析思路中,不关心图表是什么,也不关心图表如何展示,因此图表是千变万化的,比如折线图可以横过来,条形图也可以变成柱状图,因此 **你将维度放到列,就是一个柱状图,你将维度放到行,就是一个条形图** 。
|
||||
2. 将精力真正放到你要拖拽的字段上。由于字段已经有维度、度量的区别,配置区域就不要再限定维度与度量了,减少理解成本。
|
||||
3. 维度与度量可以同时放在行或列上,这是探索式分析的另一个精髓能力,看下图:
|
||||
|
||||
<img width=306 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566636365926-c5d37423-0e32-4382-ae7d-2b9760caeb97.png#align=left&display=inline&height=435&name=image.png&originHeight=1240&originWidth=872&size=94620&status=done&width=306">
|
||||
|
||||
做探索式分析功能时,要跳出思维定式:**为什么条形图的纵轴不能放维度呢?**如上图所示,如果行拖拽了两个不同的度量,那么可以出现两条线或者双轴图,但当拖拽一个维度一个度量时,可以对图表进行 **分面** ,比如观察 2013 ~ 2016 年不同顾客对销量的贡献。
|
||||
|
||||
### 行
|
||||
|
||||
表格类的行、图表类的纵轴。一般建议放置度量字段。
|
||||
|
||||
### 列
|
||||
|
||||
表格类的列、图表类的横轴。一般建议放置维度字段。
|
||||
|
||||
如上所示,无论行还是列,都可以进行任意维度度量组合,且字段数量不限,而且可以在任何层级进行下钻。**对图表来说,多个维度时需要进行分面处理:**
|
||||
|
||||
<img width=476 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566637370597-d52b7677-9ec4-40f3-aa54-a0382ac56de1.png#align=left&display=inline&height=428&name=image.png&originHeight=1448&originWidth=1612&size=106528&status=done&width=476">
|
||||
|
||||
如上图所示,将列放置两个维度字段成为柱状图,那么横轴就要同时表示两个维度,如上图所示。如果横轴还有更多的维度,可以再不断对横轴进行拆分。
|
||||
|
||||
横轴(列)多维度字段的顺序也会影响图表的展现。**上图最后一个字段是 Category 默认是离散的,所以这个离值就决定了图表使用柱状图,图表类型由维度周最后一个字段连续或离散决定。**
|
||||
|
||||
比如我们对调 Order Date 与 Category 会怎样?
|
||||
|
||||
<img width=476 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566637657990-0387a37c-f4dc-45ca-a53a-aa10b4573318.png#align=left&display=inline&height=420&name=image.png&originHeight=1456&originWidth=1620&size=137950&status=done&width=467">
|
||||
|
||||
我们得到了三个不同类目近 12 个月的趋势,之所以是折线图,因为图表的维度轴(列)是连续的。**如果我们对 Order Date 进行天级别的下钻:**
|
||||
|
||||
<img width=462 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566637772126-a9825860-4851-41ae-a56d-df984ea49b4c.png#align=left&display=inline&height=328&name=image.png&originHeight=1462&originWidth=2062&size=128020&status=done&width=462">
|
||||
|
||||
可以看到,**下钻功能本质上就是维度轴支持对多个维度字段拆分处理。只要图表支持了维度轴任意维度字段的分面展示,那么配置端就可以将下钻按照拖了多个字段的方式去理解了。**
|
||||
|
||||
**如果我们将折线图切换为表格,会发生什么?**
|
||||
|
||||
<img width=578 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566637986444-1a78b31f-6bb6-4c3e-b69b-0bd5a32c399f.png#align=left&display=inline&height=211&name=image.png&originHeight=738&originWidth=2018&size=111516&status=done&width=578">
|
||||
|
||||
我们会发现,原本存在于列的 Category 被自动挪到了行,原本存在于行的 Sales 被挪到了 “标记” 区域。在正式介绍 “标记” 区域前,先理解一下为何会发生这种转变:
|
||||
|
||||
**表格类组件是双维度组件,折线图是单维度组件。** 也就是表格的行与列都是维度,而折线图横轴作为维度后,纵轴就要作为度量。上面的例子中,折线图维度有两个字段,虽然通过分面方式渲染出来了,但当切换为支持双维度的表格后, **可以将多余的一个维度挪到表格组件另一个维度区域中**。
|
||||
|
||||
而表格行与列都是维度的情况下,单元格的值就需要用 “标记” 中文本来表示,因此原折线图的度量字段自动转移到了 “标记” 区域。
|
||||
|
||||
### 标记
|
||||
|
||||
标记区域也采取字段拖拽的方式,即对字段进行标记。
|
||||
|
||||
标记区域分为 **颜色、大小、标签、详细信息、工具提示、路径。**标记正如其名,是作用于图表上的标记,**即不会对图表框架有实质性影响的辅助标记信息。**
|
||||
|
||||
对不同图表来说,影响最大的是行与列,它能决定用什么图表,如何拆分数据。而标记往往是改变图表中辅助性元素,比如文字或者颜色等等。
|
||||
|
||||
#### 工具提示
|
||||
|
||||
不影响任何图像显示,仅仅在提示信息中新增字段信息。
|
||||
|
||||
**对图表来说,指的是 Tooltip 提示信息增加对应的字段:**
|
||||
|
||||
<img width=424 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566655533184-4ea4560f-8fef-40aa-9910-c301842c53b4.png#align=left&display=inline&height=363&name=image.png&originHeight=1000&originWidth=1168&size=91870&status=done&width=424">
|
||||
|
||||
从上图可以看到,利润字段放在工具提示区域,则图表的 Tooltip 会新增利润这个字段的信息。**值得关注的是,Tableau 所有图表都支持 Tooltip 包括表格:**
|
||||
|
||||
<img width=623 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566655642537-e7a18aa6-f619-45c4-b2b3-f95274d40107.png#align=left&display=inline&height=157&name=image.png&originHeight=374&originWidth=1480&size=42938&status=done&width=623">
|
||||
|
||||
这保证了配置统一,行为统一。
|
||||
|
||||
#### 大小
|
||||
|
||||
控制图表大小。
|
||||
|
||||
对于线图,控制线的粗细;对于气泡图控制气泡大小;对于柱状图控制柱子粗细;但是对面积图与表格没有明显作用。这得益于 Tableau 将每个图表大小属性尽可能抽象出来。
|
||||
|
||||
<img width=360 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566655966495-9241a848-b8c0-48b7-b0a4-8fbcc44ff58c.png#align=left&display=inline&height=273&name=image.png&originHeight=748&originWidth=988&size=58386&status=done&width=360">
|
||||
|
||||
<img width=360 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566656245320-7f29da9d-363f-4fd3-95f3-15b0ec2a3522.png#align=left&display=inline&height=223&name=image.png&originHeight=982&originWidth=1602&size=99303&status=done&width=364">
|
||||
|
||||
#### 文本
|
||||
|
||||
即直接展示在图表上的文本。
|
||||
|
||||
对普通图表来说,文本体现为 Label,即直接展示在图表上的文字。比如柱状图默认是没有 Label 文字的,要将对应字段拖拽到文本标记上才会出现。
|
||||
|
||||
<img width=404 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566656520351-588602a5-db01-4e46-bfdd-6f771353d7a8.png#align=left&display=inline&height=379&name=image.png&originHeight=934&originWidth=996&size=77362&status=done&width=404">
|
||||
|
||||
这体现出与普通报表构思的不同。对普通报表来说,Label 是通过一个勾选项开启的,Label 对应的值就是图表度量这个字段的值。而 Tableau 将标签值以字段方式开放拖拽,就有了展示与值分开的可能性,可适用范围更广。
|
||||
|
||||
> 有人觉得长度和数字一定要对应上,这也是对数据理解不同导致的。Tableau 将文本(标签)列在标记里,说明文本和颜色、大小一样,都是一种附加的信息展示维度,很多时候不需要两种方式展示同一种信息,反而需要图形以更多方式以不同维度展示信息。
|
||||
|
||||
#### 颜色
|
||||
|
||||
控制图表的颜色。
|
||||
|
||||
比如在度量为销量时,可以将利润作为颜色,甚至再将折扣作为文本,通过一个折线图同时看多种度量信息:
|
||||
|
||||
<img width=386 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566656981186-8f2441d6-78d0-4d34-af7f-139b0f21cb30.png#align=left&display=inline&height=344&name=image.png&originHeight=888&originWidth=996&size=82343&status=done&width=386">
|
||||
|
||||
与之对比,我们可以将利润放在右 Y 轴作为双轴图达到相同的效果:
|
||||
|
||||
<img width=439 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566657023624-c01b0008-e7d7-4991-99c1-beddc8c72762.png#align=left&display=inline&height=319&name=image.png&originHeight=886&originWidth=1218&size=112190&status=done&width=439">
|
||||
|
||||
**标记就是为了在不增加行、列字段数量基础上,通过颜色、大小、标签、工具提示等维度展示出额外信息。**
|
||||
|
||||
#### 详细信息
|
||||
|
||||
如果将度量拖拽到详细信息,会发现完全没有作用。因为 “详细信息” 只有拖拽维度字段才生效。“详细信息” 其实是用作下钻的,拖拽一个维度字段后,可以按照这个维度进行下钻。
|
||||
|
||||
<img width=533 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566657906186-3348cdf7-823f-4111-9685-dbf5fb05c0f8.png#align=left&display=inline&height=376&name=image.png&originHeight=1054&originWidth=1496&size=102786&status=done&width=533">
|
||||
|
||||
如上图所示,将销售按照产品线拆解成三条线。但这三条线无法分辨,因此可以使用颜色来拆分维度:
|
||||
|
||||
<img width=533 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566657988073-e8e50316-feb2-4c7f-98dc-45f4a2ffd624.png#align=left&display=inline&height=372&name=image.png&originHeight=1052&originWidth=1510&size=112469&status=done&width=534">
|
||||
|
||||
这样就能将拆解的内容按不同颜色展示。因此, **对标记作用的字段如果是维度字段,且作用于颜色、大小、标签、详细信息时,会额外进行维度进行拆解,并对拆解后的内容进行颜色或大小区分。**
|
||||
|
||||
相信读到这里会有个疑问:按照维度进行拆解与维度拖拽多个字段进行字段有什么区别?我们试一下看看效果,将产品类目维度拖拽到销量所在的行,对销量进行销量维度的拆分:
|
||||
|
||||
<img width=570 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566658461645-fbcc1b14-0111-47b1-b643-d23f36f96ef2.png#align=left&display=inline&height=512&name=image.png&originHeight=1058&originWidth=1178&size=92443&status=done&width=570">
|
||||
|
||||
**可以看到,在行、列进行的多维度拆分使用的是分面策略,而在标记中对维度进行拆分使用的是单图表多轴方式来实现。**
|
||||
|
||||
除此之外的区别在于,在标记进行的维度拆分默认作用于度量,而行列上的多维度拆分可以任意作用于维度或度量。
|
||||
|
||||
> 同时配置端要限制 **能拆分的只有维度或离散状态的度量** ,也就是只有离散状态的字段可以被拆分。如上图所示,我们不能将 Category 拖拽到 Sales 右侧,除非将 Sales 设置为离散类型。
|
||||
> Tips:Tables 对维度与度量分别分配了蓝色、绿色,当我们将绿色度量字段设置为离散类型时,这个度量字段会变成蓝色,也就是当作了维度字段进行处理。
|
||||
|
||||
最后,标记区域不仅能拖拽字段,还可以单击后修改详细配置,比如修改颜色详细配置:
|
||||
|
||||
<img width=220 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566658916573-5c989763-bd8f-4f2d-8f57-9f52b9c39b20.png#align=left&display=inline&height=448&name=image.png&originHeight=896&originWidth=442&size=39310&status=done&width=221">
|
||||
|
||||
或者对工具提示的 Tooltip 内容进行定制:
|
||||
|
||||
<img width=557 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566658941101-e4eac6aa-d4f1-4397-9be2-ccc424211f00.png#align=left&display=inline&height=319&name=image.png&originHeight=910&originWidth=1590&size=207889&status=done&width=557">
|
||||
|
||||
### 筛选器
|
||||
|
||||
Tableau 将所有筛选条件都收敛到筛选器中,我们可以通过拖拽字段的方式对某个字段进行筛选:
|
||||
|
||||
<img width=635 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566659069046-3e4f195e-1c9d-492d-bfe7-6f6d89997692.png#align=left&display=inline&height=210&name=image.png&originHeight=494&originWidth=1494&size=138806&status=done&width=635">
|
||||
|
||||
如上图所示,比如只看办公用品与科技产品。但其实除了这个通用功能之外,Tableau 还支持更强大的图表交互功能,即点击或圈选图表后,可以对选中的点(字段值)进行保留或排除:
|
||||
|
||||
<img width=606 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566659178209-134b5fc5-b067-4481-bf99-ed36c8c458c7.png#align=left&display=inline&height=198&name=image.png&originHeight=442&originWidth=1350&size=55242&status=done&width=606">
|
||||
|
||||
**当我们选择排除这几个点时,会自动生成一份对维度字段的筛选条件排除掉选中日期,所以图表是完全数据驱动的:** 一般来说
|
||||
|
||||
<img wdith=576 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566659259441-22ef9da8-e3bc-4cef-b32e-c538c1dfe3e0.png#align=left&display=inline&height=268&name=image.png&originHeight=696&originWidth=1494&size=189252&status=done&width=576">
|
||||
|
||||
如果属性存在下钻关系会如何呢?无论是行列中对维度的下钻,还是通过标记对维度进行了拆解,筛选都是对 **字段层系** 生效的:
|
||||
|
||||
<img width=575 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566659420072-3e6e41f4-bc99-4d7d-a93f-553abe429a94.png#align=left&display=inline&height=169&name=image.png&originHeight=442&originWidth=1504&size=155974&status=done&width=575">
|
||||
|
||||
如上图所示,对下钻后的字段进行筛选,**那么筛选条件也会自动构造出临时的字段层系,并对这个临时层系进行筛选。** 可以看到,我们不仅能在字段配置区动态组成层系字段,在筛选器中也可以生成临时层系进行筛选,我们需要支持任意层系组合的字段,并作用于筛选器、行列,甚至是标记上。
|
||||
|
||||
顺带一提,我们还可以对设置了筛选的字段层系组合拖拽到任意地方使用:
|
||||
|
||||
<img width=393 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566659649713-3428813a-d156-4c80-935f-e40de333a8fe.png#align=left&display=inline&height=434&name=image.png&originHeight=1050&originWidth=950&size=88795&status=done&width=393">
|
||||
|
||||
要处理这种场景,**我们需要让所有字段都拥有筛选能力**,普通字段等于没有筛选条件,我们也可以对一个包含了筛选条件的字段拖拽到任何位置作用。
|
||||
|
||||
刚才是对维度进行的筛选,有没有对度量进行筛选的场景呢?有,但我们只能手动将度量字段拖拽到筛选器位置进行手动筛选:
|
||||
|
||||
<img width=613 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566659919516-77548c29-7c02-4857-9eec-fb1465826f9a.png#align=left&display=inline&height=312&name=image.png&originHeight=832&originWidth=1636&size=86512&status=done&width=613">
|
||||
|
||||
如果我们进行图表内的圈选操作,增加的筛选条件一定是按维度来的:
|
||||
|
||||
<img width=613 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566659955376-66830503-bce1-443d-ab98-9fbc90e54fdb.png#align=left&display=inline&height=273&name=image.png&originHeight=756&originWidth=1694&size=80004&status=done&width=612">
|
||||
|
||||
这么理解这一行为:维度是离散的,勾选操作能表达的含义有限,比如勾选折线图的某些点,如何知道我们要勾选的是维度的那几个月,还是度量的利润范围呢?
|
||||
|
||||
<img width=613 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566660078571-ecf7c466-44c1-46a5-9ef0-d3692bf22fb8.png#align=left&display=inline&height=555&name=image.png&originHeight=1468&originWidth=1620&size=113222&status=done&width=613">
|
||||
|
||||
**由于最终勾选操作落地在点上,而不是区间上(连续值也不适合进行圈选),所以默认按对维度进行筛选是最准确的理解。**如果上图的操作意图中,你想勾选的不是 6~12 月的区间,而是销量在 13k ~ 45.5k,则需要手动拖拽利润字段,并精确输入筛选范围:
|
||||
|
||||
<img width=482 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566660226577-102ca29f-b6ef-43a7-860a-041b04274da8.png#align=left&display=inline&height=353&name=image.png&originHeight=774&originWidth=1056&size=81734&status=done&width=482">
|
||||
|
||||
值得注意的是,对连续型度量进行筛选前,还可选择聚合方式:比如对求和的值进行范围筛选,或者对最大值进行范围筛选,功能十分强大。
|
||||
|
||||
## 理解图表
|
||||
|
||||
图表是数据可视化的载体,只有数据与配置,没有各式各样的图表,很难产生直观的数据洞察。
|
||||
|
||||
可以说, **按照探索式分析的思路,当配置好数据与配置后,可以有多种可视化载体去展示这种配置信息。** 比如行、列分别拖拽了日期与销量,那么折线图、表格、散点图、柱状图都可以满足需求,但如果行所在的字段是离散的,那么折线图、散点图就不适合了,这就需要图表推荐功能根据配置推荐合适的图形展示。
|
||||
|
||||
Tableau 内置的图表分为 N 大类 - **表格、地图、柱折面饼、散点/象限图** 、以及直方图、盒须图、甘特图、靶心图等。可见分析数据,不需要太多种类可视化展现方式,但对于每个图表组件来说,都需要修炼深厚的内功,做好一个表格、折线图并不简单。
|
||||
|
||||
### 行与列
|
||||
|
||||
表格、地图、柱折面饼、散点/象限图等都可以用行与列描述基本架构:
|
||||
|
||||
- 表格天然拥有行与列,对调后则代表转置。表格的行与列必须是维度字段,如果拖拽度量字段上去会自动切换为其他图表,再切回来则会把度量字段挪动到 “文本” 标记区域中。
|
||||
- 地图行与列就是经纬度,当维度字段放到 “详细信息” 时,根据地理映射表转化为经纬度自动生成经纬度放在行与列。
|
||||
- 柱折面饼、散点/象限图都是直角坐标系的图形,以维度字段作为维度轴,以度量字段作为度量轴。
|
||||
|
||||
#### 行列的下钻
|
||||
|
||||
在行或列存在多个维度字段时,图表要进行相应下钻。表格对于行下钻如下图所示:
|
||||
|
||||
<img width=529 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566699040802-40a23a43-7a23-43d1-9d06-27a6413803e8.png#align=left&display=inline&height=314&name=image.png&originHeight=856&originWidth=1440&size=106220&status=done&width=529">
|
||||
|
||||
**上图也可以理解为展示出 Order Date 与 Order ID 的明细数据,按照 Order Date 分组且列合并。** 下钻就是一步步接近明细数据的过程,但目的不是为了看明细表,而是看某些维度下按其他维度拆分的详细信息。
|
||||
|
||||
图表下钻和表格思路是一致的:
|
||||
|
||||
<img width=529 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566699822756-b087948b-3104-4bd4-a76b-1cb2067edd65.png#align=left&display=inline&height=335&name=image.png&originHeight=1338&originWidth=2114&size=109757&status=done&width=529">
|
||||
|
||||
对于维度轴多维度下钻,将每个维度轴下钻到更细粒度。图表在行与列同时下钻时,与表格的表现稍有不同。仅从轴来看拆解方式是相同的,内部展示了多套轴:
|
||||
|
||||
<img width=667 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566700041429-03bcb68a-dd00-4cab-a256-f37234bb2300.png#align=left&display=inline&height=423&name=image.png&originHeight=1350&originWidth=2128&size=155697&status=done&width=667">
|
||||
|
||||
**可以认为,当行或列上最后一个字段为度量时,就会切换为图表展示,因为图表适合展示连续状态。** 如果排除上图蓝色区域,剩下的区域就是个交叉表,交叉表只是行与列同时存在维度字段的场景,仅有行或列时就变成了普通表格;而图形的下钻和表格下钻机理相同,只是把 “单元格” 的文本换成了柱子或线。
|
||||
|
||||
**所以对任何图表的下钻,都是对轴的下钻,** 相同的是单元格属性永远不会改变,表格的单元格是文本,图形单元格是图形,一个简单折线图可以理解为对整体行与列单元格进行 “连续打通”:
|
||||
|
||||
<img width=406 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566700448492-a7b96c99-b600-41b0-9159-e88412f4402b.png#align=left&display=inline&height=329&name=image.png&originHeight=1342&originWidth=1654&size=105794&status=done&width=406">
|
||||
|
||||
如果继续对行列添加维度进行下钻,其实是对轴进行下钻。**排除度量字段不看,就是一个交叉表的下钻过程,如下图所示蓝色框圈住的部分就是一组大的单元格**:
|
||||
|
||||
<img width=629 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566700520147-c644eae8-8c04-4c7d-82aa-b5e4e1c2e4d8.png#align=left&display=inline&height=467&name=image.png&originHeight=1340&originWidth=1806&size=163645&status=done&width=629">
|
||||
|
||||
由于最后一个字段是度量,因此在叶子结点的展开就不是表格模式的单元格,而是连续的线条了。
|
||||
|
||||
经过上面的总结,我们要意识到,在探索式分析场景对行列的下钻,表格与图表的逻辑是通用的,实现时也要整体考虑。**将轴功能抽离成通用部分来做,表格与图表的区别只是对最后一个字段单元格是离散处理还是连续处理。**
|
||||
|
||||
#### 层系的下钻
|
||||
|
||||
<img width=528 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566699587535-dd5e11f6-6fd2-42ce-be2b-e01d7dee650a.png#align=left&display=inline&height=300&name=image.png&originHeight=818&originWidth=1438&size=102881&status=done&width=528">
|
||||
|
||||
层系字段下钻与拖多个字段表现一致,但由于存在父子关系,因此在图表上可以展现出 “展开” “收起” 按钮,点击后并不是对图表本身进行操作,而是发送一个事件对 “行” 进行操作,最后通过数据驱动完成展开或收起动作。
|
||||
|
||||
#### 不适合行列的图表
|
||||
|
||||
饼图就不适合行列,因为饼图是根据离散维度进行拆分,扇叶大小可以由一个度量字段决定,因此对饼图来说,行就对应到 “颜色”、列就对应到新增的 “角度” 这个标记:
|
||||
|
||||
<img width=336 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566704242607-bab5e722-d27c-4ad5-8518-b470fddbf9da.png#align=left&display=inline&height=279&name=image.png&originHeight=558&originWidth=672&size=50344&status=done&width=336">
|
||||
|
||||
#### 没有维度轴的图表
|
||||
|
||||
只有行配置的图形推荐用表格,但柱状图、折线图也可以支持这种情况,只要把横轴忽略即可:
|
||||
|
||||
<img width=300 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566706245345-61b365db-793a-4d15-9677-b4e332381bda.png#align=left&display=inline&height=312&name=image.png&originHeight=912&originWidth=878&size=53419&status=done&width=300">
|
||||
|
||||
从样式上来看没有横轴,其实这种情况是把所有维度的横轴都聚合后的表现。
|
||||
|
||||
### 连续与离散值
|
||||
|
||||
我们分别看看连续与离散作用于维度和度量时的区别。
|
||||
|
||||
#### 作用于度量
|
||||
|
||||
图表要能适配对连续或离散值的处理。比如对销量来说,如果切换为离散值,则当成字符串展示:
|
||||
|
||||
<img width=632 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566662092893-32545363-82da-498a-868f-18a59bb41c24.png#align=left&display=inline&height=141&name=image.png&originHeight=472&originWidth=2122&size=63869&status=done&width=632">
|
||||
|
||||
如果将销量切换为连续值,则单元格就要使用线条长度代表值的大小,**即连续性的值要能够产生 “对比感”:**
|
||||
|
||||
<img width=632 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566662072534-23177d9d-f32c-4aa2-bea4-8fef0ab6e3c2.png#align=left&display=inline&height=161&name=image.png&originHeight=534&originWidth=2106&size=58097&status=done&width=635">
|
||||
|
||||
上图组件是表格,本身适合展示离散值,但可以看到对连续值展示做了适配。对于适合展示连续值的图形,则无法做离散适配:
|
||||
|
||||
<img width=200 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566703397222-9da012d8-9d13-4378-9d5a-50903f3ba777.png#align=left&display=inline&height=253&name=image.png&originHeight=976&originWidth=772&size=48952&status=done&width=200">
|
||||
|
||||
比如这个柱状图,如果将销量切换为离散,则会自动切换到表格,因为对于双离散值用柱折面饼展示是无意义的。
|
||||
|
||||
#### 作用于维度
|
||||
|
||||
如上图所示,就是维度使用了离散字段的例子,由于维度是离散的,因此使用柱状图展示,因为柱子间也是隔离的。
|
||||
|
||||
**对于连续型字段作用于维度,默认适合散点图,因为散点图的行与列都是度量,适合作为默认推荐:**
|
||||
|
||||
<img width=283 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566703779092-ce78066e-75ec-4b89-9fc1-f44c135e5b5d.png#align=left&display=inline&height=312&name=image.png&originHeight=1154&originWidth=1048&size=66333&status=done&width=283">
|
||||
|
||||
但能用散点图的就也能用线图, **当维度是连续日期字段时,适合用折线图而不是散点图。**因为日期虽然连续,但 **本身不适合做比较** ,因此作为一种连续型维度展示比较合适;而散点图两个轴都适合连续型度量,因此不适合方日期这种连续型维度字段。
|
||||
|
||||
<img width=406 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566703754082-6ec2f472-ea42-4003-aab3-c07f47b8a340.png#align=left&display=inline&height=289&name=image.png&originHeight=1166&originWidth=1636&size=96183&status=done&width=406">
|
||||
|
||||
当然也具备将折线图随时切换为散点图的能力,但这种图形没有什么业务价值:
|
||||
|
||||
<img width=406 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566704003468-7ba3e545-9abf-470f-841d-44dd42e1f754.png#align=left&display=inline&height=292&name=image.png&originHeight=1178&originWidth=1654&size=97442&status=done&width=410">
|
||||
|
||||
因此我们对折线图进行标记:行适合连续型维度字段,对散点图进行标记:行列都适合连续型度量字段,就可以根据配置 **实现推荐图表的功能**。
|
||||
|
||||
### 标记
|
||||
|
||||
除了饼图支持 “角度”、线图支持 “路径” 这些特殊标记外,所有图表都支持下面五种通用标记:“工具提示”、“大小”、“文本”、“颜色”、“详细信息”。
|
||||
|
||||
**工具提示** 比较简单,所有图表都支持鼠标 Hover 后弹出 Tooltip 即可,并且这个 Tooltip 允许自定义和拓展工具提示字段。
|
||||
|
||||
**大小** 则只有折、柱、散三种图支持,因为这三种图分别有可以描述的大小的线条粗细、柱子宽度、圆圈半径。
|
||||
|
||||
**文本** 对应柱折面饼的 Label、对应表格,矩形树状图,地图的 **单元格内容。**
|
||||
|
||||
**颜色、详细信息** 则比较特殊,下面详细说明:
|
||||
|
||||
**拖拽已有字段到详细信息 - 没有任何效果:**
|
||||
|
||||
<img width=476 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566705683330-21e04dfb-41f7-45dc-92b8-69dab98bf009.png#align=left&display=inline&height=249&name=image.png&originHeight=740&originWidth=1416&size=73008&status=done&width=476">
|
||||
|
||||
因为本身就在看这个字段的详细信息,因此没有效果。
|
||||
|
||||
**但如果拖拽已有字段到颜色,则可以根据数值大小或分类进行按颜色区分:**
|
||||
|
||||
<img width=479 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566705734424-b2bc735d-35fa-4cf9-8b67-e24e3dccd0f5.png#align=left&display=inline&height=250&name=image.png&originHeight=736&originWidth=1412&size=76772&status=done&width=479">
|
||||
|
||||
等于开启了图表筛选功能,当颜色筛选条件字段是连续型时,出现筛选滑块,**是离散型时,出现图例:**
|
||||
|
||||
<img wdith=479 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566705795546-6a5e79b7-b32a-4c37-90bc-43926a3a6283.png#align=left&display=inline&height=245&name=image.png&originHeight=720&originWidth=1408&size=80186&status=done&width=480">
|
||||
|
||||
**如果拖拽字段不存在于行和列上,对于度量字段,会根据值进行颜色排序(度量拖拽到详细信息依然没有效果):**
|
||||
|
||||
<img width=479 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566705874311-ebc4ec5d-021f-4601-9f8d-d5becfa9b1ce.png#align=left&display=inline&height=244&name=image.png&originHeight=720&originWidth=1422&size=79431&status=done&width=482">
|
||||
|
||||
如上图所示,我们可以从长度看利润,从颜色深度看销量。
|
||||
|
||||
**如果拖拽字段不存在于行和列上,且是维度字段,则会先进行维度拆分,之后如果选择的是 “颜色” 标记区域,还会对同一组的拆分标记颜色区分。**
|
||||
|
||||
**由于标记区域对维度的拆分是不分行于列的,因此每个图表会根据自身情况进行合适的拆分。**
|
||||
|
||||
比如条形图如果按某个新维度拆分,则会采取 “堆积柱状图” 的策略:
|
||||
|
||||
<img width=471 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566706116218-919d2ce7-3fb2-4a32-8624-a3f6cb3122fb.png#align=left&display=inline&height=327&name=image.png&originHeight=986&originWidth=1422&size=117354&status=done&width=471">
|
||||
|
||||
如果是折线图,则会采取 “多条线” 的策略:
|
||||
|
||||
<img width=471 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566706404581-01866c81-0553-4245-9b56-176bc5708bfe.png#align=left&display=inline&height=319&name=image.png&originHeight=960&originWidth=1416&size=127313&status=done&width=471">
|
||||
|
||||
如果是散点图,只要将拆分后多出来的点打散出来即可。由于散点图的维度拆分不像折线图和柱状图可以分段,因此如果不采用按颜色打散,是无法分辨分组的:
|
||||
|
||||
<img width=471 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566706443331-c817102a-5134-493f-bddf-04fc73f9548f.png#align=left&display=inline&height=319&name=image.png&originHeight=958&originWidth=1414&size=107140&status=done&width=471">
|
||||
|
||||
之所以说探索式分析的复杂度很高,是因为其可能性公式为:
|
||||
|
||||
**字段 x 离散连续 x 行列 x 行列下钻 x 标记种类 x 筛选 x 图表**
|
||||
|
||||
这种组合的笛卡尔积几乎是无穷无尽的。
|
||||
|
||||
### 轴交互
|
||||
|
||||
图表一些特定功能是隐藏在轴交互里的。拿折线图来说,一共有 5 个拖拽交互位置,如下图所示:
|
||||
|
||||
<img width=439 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566706982072-996f5f6e-b922-40ec-8066-761857fb2e30.png#align=left&display=inline&height=354&name=image.png&originHeight=1324&originWidth=1642&size=100488&status=done&width=439">
|
||||
|
||||
一般这些区域是用来拖拽度量字段的,所以如果拖拽了维度字段过来,最终会被归类到行列或标记上。
|
||||
|
||||
#### 拖拽维度
|
||||
|
||||
**维度拖拽到底部 1 区域等于替换列字段** :
|
||||
|
||||
<img width=195 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566707169109-6e38483a-123a-4064-81ff-fd0ed82010ed.png#align=left&display=inline&height=344&name=image.png&originHeight=1010&originWidth=572&size=44840&status=done&width=195">
|
||||
|
||||
**维度拖拽到图表中 4 区域等于拖到了颜色标记** :
|
||||
|
||||
<img width=387 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566707229005-d6ed84f8-8639-4dc6-96b0-ae2bebe70dc0.png#align=left&display=inline&height=331&name=image.png&originHeight=974&originWidth=1138&size=111933&status=done&width=387">
|
||||
|
||||
**维度拖拽到左侧 3 区域等于对行进行下钻:**
|
||||
|
||||
<img width=406 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566707279972-a22eb90b-2ebc-4727-911c-10c1d329a06c.png#align=left&display=inline&height=285&name=image.png&originHeight=1012&originWidth=1444&size=106415&status=done&width=406">
|
||||
|
||||
同理拖拽到最上面区域等于对列进行下钻。
|
||||
|
||||
#### 拖拽度量
|
||||
|
||||
让我们看看拖拽度量时的情况。度量能拖拽的范围更多。**比如拖拽到右轴 5 区域,则形成了双轴图:**
|
||||
|
||||
<img width=447 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566707391460-e528c513-bc22-4b02-9717-e9339b83d419.png#align=left&display=inline&height=360&name=image.png&originHeight=994&originWidth=1234&size=120248&status=done&width=447">
|
||||
|
||||
**拖拽到左侧 2 区域则表示在图中额外增加一个轴:**
|
||||
|
||||
<img width=571 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566707459746-8af4aa6e-acf4-4600-8fce-32c5fbe3400c.png#align=left&display=inline&height=333&name=image.png&originHeight=1020&originWidth=1748&size=126450&status=done&width=571">
|
||||
|
||||
要注意的是,上图的行显示 “度量值”,这是个特殊的字段,并通过筛选器筛选出拖拽的两个字段 Profit 和 Sales。除了拖拽以外,还可以通过将左侧 “度量值” 字段直接拖入行实现:
|
||||
|
||||
<img width=579 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566707593546-3670ea98-6e42-4fd2-8438-402c42c318d6.png#align=left&display=inline&height=275&name=image.png&originHeight=1042&originWidth=2190&size=243455&status=done&width=579">
|
||||
|
||||
如上图所示,将度量值放到行,并按度量名称进行颜色标记,就得到了拖拽度量到左侧 2 区域的效果。 **这也说明了所有图表交互最终都是通过映射到配置完成,所有能拖拽的操作都可以通过配置配出来** 。
|
||||
|
||||
对表格来说,能拖拽的区域是行、列、单元格:
|
||||
|
||||
<img width=521 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566707733747-55ea9f8c-d2c2-4766-b7b1-57d7abb351d2.png#align=left&display=inline&height=132&name=image.png&originHeight=358&originWidth=1416&size=29996&status=done&width=521">
|
||||
|
||||
拖拽到行或列于拖拽到字段配置区域的行或列没有区别,拖拽到单元格等于拖拽到文本标记区域。通过图表于配置区域结合的方式,即便不完全理解配置的人也可以通过将字段拖拽到图表上得到直观的操作感。
|
||||
|
||||
### 点击、圈选交互
|
||||
|
||||
所有图表都支持点击、圈选的方式选中 “点”。对表格来说,点就是单元格:
|
||||
|
||||
<img width=505 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566707894327-f5b88abb-d84e-4839-9c48-5fbbc9ccd929.png#align=left&display=inline&height=128&name=image.png&originHeight=356&originWidth=1408&size=42932&status=done&width=505">
|
||||
|
||||
对柱状图来说,点就是柱子:
|
||||
|
||||
<img width=307 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566707923552-680d890f-bc57-4692-b29e-ae787522bfb6.png#align=left&display=inline&height=268&name=image.png&originHeight=972&originWidth=1114&size=85024&status=done&width=307">
|
||||
|
||||
|
||||
对折线图来说,点就是节点:
|
||||
|
||||
<img width=427 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566707968058-fd8a598a-48fe-45c8-99f9-94c09f686435.png#align=left&display=inline&height=189&name=image.png&originHeight=640&originWidth=1428&size=59096&status=done&width=421">
|
||||
|
||||
对饼图来说,点就是扇叶:
|
||||
|
||||
<img width=374 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566707997384-1f1e4755-0863-48de-8abc-f56ae9feaad1.png#align=left&display=inline&height=169&name=image.png&originHeight=338&originWidth=748&size=26955&status=done&width=374">
|
||||
|
||||
所有的点被选中后都有基本高亮功能,最重要的是能对选中的点进行保留、排除、局部排序等等。
|
||||
|
||||
**比如我们可以对上图饼图选中的几个扇形区域进行从小到大排序:**
|
||||
|
||||
<img width=316 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566708115948-0dc7a73e-97bd-4099-b8bc-12dc16f56501.png#align=left&display=inline&height=243&name=image.png&originHeight=568&originWidth=740&size=48022&status=done&width=316">
|
||||
|
||||
我们也可以排除某些点,这个在配置章节有提到过,这个操作最终将转化为新增筛选条件:
|
||||
|
||||
<img width=283 src="https://cdn.nlark.com/yuque/0/2019/png/201572/1566708205782-b74a2d1f-8141-4a05-bea4-8824240a5e67.png#align=left&display=inline&height=280&name=image.png&originHeight=658&originWidth=666&size=52520&status=done&width=283">
|
||||
|
||||
最后,选中状态在单图表中看似只有高亮效果,但是在多图表联动时,高亮的选中区域会组成一个临时的筛选条件,作用于所有相同数据集的图表,并对这些图表的筛选结果做高亮处理。
|
||||
|
||||
# 3. 总结
|
||||
|
||||
理解了探索模型对数据、配置、图表的理解,就能学会探索式思维分析数据,对制作探索式 BI 也有借鉴意义。
|
||||
|
||||
> 讨论地址是:[精读《Tableau 探索式模型》 · Issue #199 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/199)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
|
||||
|
||||
> 关注 **前端精读微信公众号**
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
|
||||
|
||||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
|
||||
@@ -0,0 +1,111 @@
|
||||
> 作者:五灵
|
||||
|
||||
本周工作中遇到类似颜色主题的问题,在查资料的时候,看到这个视频,觉得讲得很清楚,而且趣味性丰富,所以想拿出来讲讲这个很有意思的主题。
|
||||
|
||||
视频链接: [CSSconf EU 2018 | Dag-Inge Aas & Ida Aalen: Generating Colors with JS and CSS Custom Properties](https://www.youtube.com/watch?v=zi6L0ZqrKfA)
|
||||
|
||||
## 1. 精读
|
||||
|
||||
### CSS 变量
|
||||
|
||||
CSS 变量及 CSS Variables(Custom Properties),目前几乎都已经被主流浏览器所支持,但是估计还有一部分读者不熟悉这个功能,简单列举一下使用方法:
|
||||
|
||||
```css
|
||||
:root {
|
||||
--bg-color: brown; // 定义颜色变量
|
||||
}
|
||||
.btn {
|
||||
// 直接使用颜色预定义的颜色变量
|
||||
background-color: var(--bg-color);
|
||||
}
|
||||
```
|
||||
|
||||
### Web 内容无障碍指南的对比度
|
||||
|
||||
Web 内容无障碍指南的对比度指的是 W3C 组织发布的 [《Web Content Accessibility Guidelines (WCAG)》](https://www.w3.org/TR/WCAG/#glossary),这个指南中涵盖了让 Web 内容更易于访问的各种建议,其中针对网页的颜色对比度发布了规范。
|
||||
|
||||
在 Chrome 中对于颜色编辑的时候,打开颜色选择器也会看到当前颜色的对比度值(Contrast ratio)。
|
||||
|
||||

|
||||
|
||||
网页颜色的对比度值在 1:1 到 21:1 之间,文本和图像文本的的对比度最小值为 4.5:1,也就是说低于这个值得对比度都不符合标准。 我们看一下列举的几种颜色对比度,对比度越高,也越有利于阅读。对比度越低,对于一些存在视力障碍或色觉缺陷的用户,可能就无法阅读。
|
||||
|
||||

|
||||
|
||||
### 演讲中的颜色解决方案
|
||||
|
||||
演讲在最开始首先讲了挪威的一个法律,不符合 Web 内容无障碍指南的站点在挪威是非法的,所以挪威的 Web 开发者非常注重站点的内容无障碍。
|
||||
|
||||
首先讲了使用 css 变量的方式,支持各种颜色主题的切换。 利用 js 去设置颜色变量,支持主题的颜色切换。
|
||||
|
||||
但是紧接着就提出了问题,如果用户可以随意切换颜色主题背景色,那一些按钮的文字可读性如何去保障呢?如果用户选择了与按钮颜色想接近的背景色,我们又该怎么处理了,紧接着这个演讲给出了根据明度决定按钮文字颜色是黑色还是白色的方案。
|
||||
|
||||
- 根据明度决定是黑色还是白色
|
||||
|
||||
具体代码如下,大致原理是把彩色转为灰度的颜色,有一个著名的心理学公式:`Gray = R*0.299 + G*0.587 + B*0.114`,然后在根据颜色灰度决定使用黑色的主题还是白色的主题。
|
||||
|
||||
```javascript
|
||||
if (red*0.299 + green*0.587 + blue*0.114) > 186 use #000000 else use #ffffff
|
||||
```
|
||||
|
||||

|
||||
|
||||
可读性的问题解决了,但是紧接着又遇到了一个问题,如果用户选取的颜色很浅呢,与背景颜色的对比度小于 4.5,该怎么处理呢。
|
||||
|
||||

|
||||
|
||||
- 寻找对比度更强的颜色,增强可读性
|
||||
|
||||
演讲中给出的解决方法是不断的加深当前用户选择的颜色,循环获取到对比度最高的同色系颜色。代码如下:
|
||||
|
||||

|
||||
|
||||
获取了一个更深的颜色后,通过给按钮加一个外边框的方式,优化整体的可读性。
|
||||
|
||||

|
||||
|
||||
文章最后还介绍了,通过给定一个主题色,获取第二第三主题色的方式,通过将颜色放到 HSL 的颜色轮上,转动 hue 的值 60 度,得到一个新的第二主题色。不过演讲者也没有说清楚为什么要这么做,只是说了这么做是出于经验,觉得这样能够得到一个恰当的主题色盘。
|
||||
|
||||
### 衍生的纯 css 解决方案
|
||||
|
||||
演讲中提供颜色变更的解决方案基本都是基于 JS 计算的,后来有人在 [css-tricks](https://css-tricks.com/switch-font-color-for-different-backgrounds-with-css/) 抛出一篇文章说,这个功能基于 css 就可以完全实现,其实关于颜色的原理都是一致的,只是觉得这个实现更加 magic,但是功能都能够完全满足。比如这篇文章中,关于根据明度决定按钮文字是黑色还是白色的代码如下:
|
||||
|
||||
```css
|
||||
:root {
|
||||
--light: 80;
|
||||
/* 文字颜色变化的临界值 */
|
||||
--threshold: 60;
|
||||
}
|
||||
.btn {
|
||||
/* 会被解析成黑色或者白色 */
|
||||
--switch: calc((var(--light) - var(--threshold)) * -100%);
|
||||
color: hsl(0, 0%, var(--switch));
|
||||
}
|
||||
```
|
||||
|
||||
### 可视化图表对于颜色的应用
|
||||
|
||||
在可视化图表当中,对于颜色的应用要比 Web 要谨慎的多。我们在做 Web 开发的时候,也不妨来看一下可视化图表当中对于颜色应用的一些规范。在可视化图表中,选择的颜色不可以过于随意,每次颜色的变更都是图表信息的改变,都为图表增加了新的数据,图表的每一种颜色也是要表达的信息。列举一些图表中的颜色使用规范,比如:
|
||||
|
||||
1. 不建议使用多种颜色表达同种数据
|
||||
2. 在多条行图表中,不要使用不同的颜色或颜色轮中对立面的颜色。颜色对比过强会使读者无法专心于数据。
|
||||
3. 一般而言,应避免颜色的主体性表现,避免使用具有特殊意义的颜色。比如使用红色和绿色表示销售额的变化。
|
||||
|
||||
当然对于可视化图表来说,并不是遵循了一些色彩使用的准则,就可以得到一个优雅呈现的可视化图表。注重图表呈现的最重要的视觉元素,在视觉信息角度减少用户,减少用户视觉疲劳也很重要。
|
||||
|
||||
## 3. 相关链接
|
||||
|
||||
CSS 前景背景自动配色技术简介: [https://www.zhangxinxu.com/wordpress/2018/11/css-background-color-font-auto-match/](https://www.zhangxinxu.com/wordpress/2018/11/css-background-color-font-auto-match/)<br />
|
||||
纯 css 解决方案:[https://css-tricks.com/switch-font-color-for-different-backgrounds-with-css/](https://css-tricks.com/switch-font-color-for-different-backgrounds-with-css/)<br />
|
||||
获取颜色的 Demo: [https://confrere.com/a11y/test/](https://confrere.com/a11y/test/)<br />
|
||||
颜色色盘推荐的文章:[https://blog.graphiq.com/finding-the-right-color-palettes-for-data-visualizations-fcd4e707a283](https://blog.graphiq.com/finding-the-right-color-palettes-for-data-visualizations-fcd4e707a283)
|
||||
|
||||
> 讨论地址是:[精读《使用 css 变量生成颜色主题》 · Issue #203 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/203)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
|
||||
|
||||
> 关注 **前端精读微信公众号**
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
|
||||
|
||||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
|
||||
@@ -0,0 +1,79 @@
|
||||
> 作者:五灵
|
||||
|
||||
## 简介
|
||||
|
||||
其实关于前端深水区的讨论,已经有了很多,也有了很多相关的文章。我也想借这篇关于深水区的讨论文章,讲一下自己对于深水区的理解。
|
||||
原文链接:[技术路线:前端开发已进入深水区](https://www.yuque.com/sxc/front/kvokg4)
|
||||
|
||||
本期精读,[@camsong](https://github.com/camsong)、[@arcthur](https://github.com/arcthur)、[@ascoders](https://github.com/ascoders) 都有贡献观点。
|
||||
|
||||
## 概述
|
||||
|
||||
原文对于深水区的想法,讲的很清楚,还是建议读者去读一下原文。
|
||||
对比 2010 年,整个前端生态已经翻新了好几遍,直到近几年的 Node BFF、IDE Cloud,抑或是客户端 AI,还是 Serverless 的建设,前端想要深度参与的话,单纯依靠原来的 HTML/CSS/JS 三件套技能也远远不够了。再抛开技术,整个互联网创业生态也重构了好几遍。无论是技术层面还是意识层面,如今的前端开发已经进入深水区。
|
||||
|
||||
- 深水区需要哪些技能
|
||||

|
||||
深水区需要是四个核心能力,分别是:技术、产品、业务和管理能力。
|
||||
- 面对深水压力不需紧张
|
||||
其实何止前端开发,整个技术行业都已步入深水区,只是前端工程师的感知来的晚一些而已。只要把眼光投向深水区,问题就会一个接一个的浮上来,当越来越多问题浮起来的时候,就是你慢慢沉向深水区的时候,这时候不需要太过紧张。
|
||||
|
||||
## 精读
|
||||
|
||||
深水区的理解首先需要达成一致,并不只是一个维度的加深,而是全方位多方面的困难同时加击,压强升高、光线减少、温度剧变等等。
|
||||
|
||||
对应到文中总结的解法就是需要『技术创新、流程优化、团队合作、影响大盘、驱动业务、商业决策和团队管理』。但你展开想一下,把这个角色换成后端、无线端、甚至是 UED,是不是也能完美匹配。所以这些能力应该是技术人员发展到一定程度面临的普遍问题而不仅仅是前端。
|
||||
|
||||
但这些能力是否有个更好的概括?当然有,就是明确一个方向并带领一群人完成目标并实线商业价值。这其实就是商业或者说业务的整个运作过程。
|
||||
|
||||
这其实也在抛一个命题,前端发展到一定程度就一定要转业务吗?
|
||||
是也不是。当然要转,但并不是全转。全转业务你过去的积累有什么用?不转业务单纯前端能发挥的影响力就会受限。所以答案是利用前端技术优势同时补充业务能力推动商业流程。
|
||||
|
||||
所以此文并不是严格上讲前端技术的深水区,或者作者肯定认为他能接触的前端技术已经到瓶颈,且没有想到突破口。
|
||||
|
||||
怎么去定义深水区,@流形 认为是需要建立技术壁垒或学术壁垒。当我们看待一向技术,如果在投入一到两年就可以对齐,那么显然技术本身的深度是可观的,如果是十年才能对齐,这时候除了会影响经济或政治外,不会有人会去重做,只能使用。用另一个类似的概念反摩尔定律来对应深水区说,每隔两年,技术不能显著带来效能的成倍提升。
|
||||
|
||||
### 深水区值得关注的方向
|
||||
|
||||
#### 业务领导力
|
||||
|
||||
也就是原文提到的 “技术创新、流程优化、团队合作、影响大盘、驱动业务、商业决策、团队管理” 等能力,一个拥有领导力的人发挥的价值远超自身孤立的价值。
|
||||
|
||||
#### 业务价值
|
||||
|
||||
发挥业务价值是技术人的最终目标,比如数据库技术想发挥业务价值,就要做到高效、稳定,价值越大往往技术难度就越大。
|
||||
|
||||
值得庆幸的是,前端的业务价值与技术难度往往不成正比,有时候将客户的业务场景固化成一套模版,整合起来赋能给更多客户,这等于将商业模型作为能力赋予了其他客户,但本身并没有用到一些高级技术。前端能做的不仅是内部提效和外部体验,因为前端是人机交互的入口,才有机会将业务思考打包到代码中,直接透出给客户。
|
||||
|
||||
#### 端技术的发展
|
||||
|
||||
1. 数字孪生。那么在端上的仿真能力需要大幅提高,那么结合模型自动生成,不同物体的建模能力等都是很大挑战
|
||||
2. 虚拟实现。这点上就不赘述,从 FB 重点发展 Oculus,微软发展 HoloLens 可以看到这个趋势,从互动的未来来看,这不是终局,但是最适合今天要突破的技术。
|
||||
3. 可视分析。数据在人类面前还是过分难懂,结合数据的分析系统在各行各业正在渗透,端上结合可视化的能力就显得非常重要。
|
||||
4. 更多的,像边缘计算,前端安全等领域都是非常深入的领域。
|
||||
这些问题,已经不是一年就能完全突破的,需要 3-5 年,甚至 10 年时间。
|
||||
|
||||
#### 前端深入体系
|
||||
|
||||
1. 但对于我所处的大数据环境来说,确实接触了前端技术深水区。来源于端计算能力 + 网络基建 + 大数据的爆炸式增长。
|
||||
编辑器:复杂的开发离不开代码,前端们一直孜孜不倦的把 IDE 引入 web,VS Code 做了很成功的尝试但还是需要一层壳套着。且对于大数据处理这样的领域,需要定制的能力远超过通用的 Manaco editor 等能提供。
|
||||
2. 表格类数据处理能力:比尔盖茨最引以为豪的微软软件是 Excel。你永远不知道 Excel 有多少种酷的用法来解决用户问题。能否把 Excel 引入到 web?同时对数百万条数据做交叉分析,这对性能和架构都有很大的挑战。
|
||||
3. 可视化数据展现:大数据的一个典型特征就是价值稀疏性,如何把蕴含的价值展现出来,需要了解图形学、统计学、交互色彩等各种能力。大学老师教的内容终于能派生用场了。
|
||||
|
||||
## 总结
|
||||
|
||||
在局部领域前端已经有可能深入,当然前端技能上说这些也不能用 HTML, CSS, JS 来解决,需要开发者有深入学科的背景。但今天前端面向还是产品功能的需要,在端上更强调的还是产品功能为主。我们做一款复杂产品,更多还会在工程上纠结。如果没在功能的深入性上思考更多,以对应真正技术发展,那么深水区还远。
|
||||
|
||||
正如前面所说,深水区会压强升高、光线减少、温度剧变,需要自己发光发热和更多的坚持。
|
||||
|
||||
跨过深水区,让其他人处在浅水区就能做事,这或许就是你走出深水区的标志。就像 Alan Perlis 说的一句话『简单不先于复杂,而是在复杂之后』,也许未来看来你今天挣扎的深水区只是个小泥坑。
|
||||
|
||||
> 讨论地址是:[精读《前端深水区》 · Issue #193 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/193)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
|
||||
|
||||
> 关注 **前端精读微信公众号**
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
|
||||
|
||||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
|
||||
@@ -107,7 +107,7 @@ connect(props => ({
|
||||
|
||||
## HOC 的具体实践
|
||||
|
||||
HOC 在真实场景下的运行非常多,之前笔者在 [基于Decorator的组件扩展实践](https://zhuanlan.zhihu.com/p/22054582) 一文中也提过使用高阶组件将更细粒度的组件组合成 Selector 与 Search。结合精读文章,这次让我们通过 Form 组件的抽象来表现 HOC 具有的良好扩展机制。
|
||||
HOC 在真实场景下的运行非常多,之前笔者在 [基于 Decorator 的组件扩展实践](https://zhuanlan.zhihu.com/p/22054582) 一文中也提过使用高阶组件将更细粒度的组件组合成 Selector 与 Search。结合精读文章,这次让我们通过 Form 组件的抽象来表现 HOC 具有的良好扩展机制。
|
||||
|
||||
Form 中会包含各种不同的组件,常见的有 Input、Selector、Checkbox 等等,也会有根据业务需求加入的自定义组件。Form 灵活多变,从功能上看,表单校验可能为单组件值校验,也可能为全表单值校验,可能为常规检验,比如:非空、输入限制,也可能需要与服务端配合,甚至需要根据业务特点进行定制。从 UI 上看,检验结果显示的位置,可能在组件下方,也可能是在组件右侧。
|
||||
|
||||
@@ -115,7 +115,7 @@ Form 中会包含各种不同的组件,常见的有 Input、Selector、Checkbo
|
||||
|
||||

|
||||
|
||||
至于 HOC 在 Form 上的具体实现,首先将表单中的组件(Input、Selector...)与相应 validator 与组件值回调函数名(trigger)传入 Decorator,将 validator 与 trigger 相绑定。Decorator 完成了各种不同组件与 Form 内置 Store 间 value 的传递、校验功能的抽象,即精读文章中提到 Props Proxy 方式的其中两种作用:**提取state** 与 **操作props**
|
||||
至于 HOC 在 Form 上的具体实现,首先将表单中的组件(Input、Selector...)与相应 validator 与组件值回调函数名(trigger)传入 Decorator,将 validator 与 trigger 相绑定。Decorator 完成了各种不同组件与 Form 内置 Store 间 value 的传递、校验功能的抽象,即精读文章中提到 Props Proxy 方式的其中两种作用:**提取 state** 与 **操作 props**
|
||||
|
||||
```javascript
|
||||
function formFactoryFactory({
|
||||
@@ -0,0 +1,290 @@
|
||||
## 简介
|
||||
|
||||
React 16.8 于 2019.2 正式发布,这是一个能提升代码质量和开发效率的特性,笔者就抛砖引玉先列出一些实践点,希望得到大家进一步讨论。
|
||||
|
||||
然而需要理解的是,没有一个完美的最佳实践规范,对一个高效团队来说,稳定的规范比合理的规范更重要,因此这套方案只是最佳实践之一。
|
||||
|
||||
## 精读
|
||||
|
||||
### 环境要求
|
||||
|
||||
- 拥有较为稳定且理解函数式编程的前端团队。
|
||||
- 开启 ESLint 插件:[eslint-plugin-react-hooks](https://www.npmjs.com/package/eslint-plugin-react-hooks)。
|
||||
|
||||
### 组件定义
|
||||
|
||||
Function Component 采用 `const` + 箭头函数方式定义:
|
||||
|
||||
```tsx
|
||||
const App: React.FC<{ title: string }> = ({ title }) => {
|
||||
return React.useMemo(() => <div>{title}</div>, [title]);
|
||||
};
|
||||
|
||||
App.defaultProps = {
|
||||
title: 'Function Component'
|
||||
}
|
||||
```
|
||||
|
||||
上面的例子包含了:
|
||||
|
||||
1. 用 `React.FC` 申明 Function Component 组件类型与定义 Props 参数类型。
|
||||
2. 用 `React.useMemo` 优化渲染性能。
|
||||
3. 用 `App.defaultProps` 定义 Props 的默认值。
|
||||
|
||||
#### FAQ
|
||||
|
||||
> 为什么不用 React.memo?
|
||||
|
||||
推荐使用 `React.useMemo` 而不是 `React.memo`,因为在组件通信时存在 `React.useContext` 的用法,这种用法会使所有用到的组件重渲染,只有 `React.useMemo` 能处理这种场景的按需渲染。
|
||||
|
||||
> 没有性能问题的组件也要使用 useMemo 吗?
|
||||
|
||||
要,考虑未来维护这个组件的时候,随时可能会通过 `useContext` 等注入一些数据,这时候谁会想起来添加 `useMemo` 呢?
|
||||
|
||||
> 为什么不用解构方式代替 defaultProps?
|
||||
|
||||
虽然解构方式书写 `defaultProps` 更优雅,但存在一个硬伤:对于对象类型每次 Rerender 时引用都会变化,这会带来性能问题,因此不要这么做。
|
||||
|
||||
### 局部状态
|
||||
|
||||
局部状态有三种,根据常用程度依次排列: `useState` `useRef` `useReducer` 。
|
||||
|
||||
#### useState
|
||||
|
||||
```tsx
|
||||
const [hide, setHide] = React.useState(false);
|
||||
const [name, setName] = React.useState('BI');
|
||||
```
|
||||
|
||||
状态函数名要表意,尽量聚集在一起申明,方便查阅。
|
||||
|
||||
#### useRef
|
||||
|
||||
```tsx
|
||||
const dom = React.useRef(null);
|
||||
```
|
||||
|
||||
`useRef` 尽量少用,大量 Mutable 的数据会影响代码的可维护性。
|
||||
|
||||
但对于不需重复初始化的对象推荐使用 `useRef` 存储,比如 `new G2()` 。
|
||||
|
||||
#### useReducer
|
||||
|
||||
局部状态不推荐使用 `useReducer` ,会导致函数内部状态过于复杂,难以阅读。 `useReducer` 建议在多组件间通信时,结合 `useContext` 一起使用。
|
||||
|
||||
#### FAQ
|
||||
|
||||
> 可以在函数内直接申明普通常量或普通函数吗?
|
||||
|
||||
不可以,Function Component 每次渲染都会重新执行,常量推荐放到函数外层避免性能问题,函数推荐使用 `useCallback` 申明。
|
||||
|
||||
### 函数
|
||||
|
||||
所有 Function Component 内函数必须用 `React.useCallback` 包裹,以保证准确性与性能。
|
||||
|
||||
```tsx
|
||||
const [hide, setHide] = React.useState(false);
|
||||
|
||||
const handleClick = React.useCallback(() => {
|
||||
setHide(isHide => !isHide)
|
||||
}, [])
|
||||
```
|
||||
|
||||
`useCallback` 第二个参数必须写,[eslint-plugin-react-hooks](https://www.npmjs.com/package/eslint-plugin-react-hooks) 插件会自动填写依赖项。
|
||||
|
||||
### 发请求
|
||||
|
||||
发请求分为操作型发请求与渲染型发请求。
|
||||
|
||||
#### 操作型发请求
|
||||
|
||||
操作型发请求,作为回调函数:
|
||||
|
||||
```tsx
|
||||
return React.useMemo(() => {
|
||||
return (
|
||||
<div onClick={requestService.addList} />
|
||||
)
|
||||
}, [requestService.addList])
|
||||
```
|
||||
|
||||
#### 渲染型发请求
|
||||
|
||||
渲染型发请求在 `useAsync` 中进行,比如刷新列表页,获取基础信息,或者进行搜索, **都可以抽象为依赖了某些变量,当这些变量变化时要重新取数** :
|
||||
|
||||
```tsx
|
||||
const { loading, error, value } = useAsync(async () => {
|
||||
return requestService.freshList(id);
|
||||
}, [requestService.freshList, id]);
|
||||
```
|
||||
|
||||
### 组件间通信
|
||||
|
||||
简单的组件间通信使用透传 Props 变量的方式,而频繁组件间通信使用 `React.useContext` 。
|
||||
|
||||
以一个复杂大组件为例,如果组件内部拆分了很多模块, **但需要共享很多内部状态** ,最佳实践如下:
|
||||
|
||||
#### 定义组件内共享状态 - store.ts
|
||||
|
||||
```tsx
|
||||
export const StoreContext = React.createContext<{
|
||||
state: State;
|
||||
dispatch: React.Dispatch<Action>;
|
||||
}>(null)
|
||||
|
||||
export interface State {};
|
||||
|
||||
export interface Action { type: 'xxx' } | { type: 'yyy' };
|
||||
|
||||
export const initState: State = {};
|
||||
|
||||
export const reducer: React.Reducer<State, Action> = (state, action) => {
|
||||
switch (action.type) {
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
#### 根组件注入共享状态 - main.ts
|
||||
|
||||
```tsx
|
||||
import { StoreContext, reducer, initState } from './store'
|
||||
|
||||
const AppProvider: React.FC = props => {
|
||||
const [state, dispatch] = React.useReducer(reducer, initState);
|
||||
|
||||
return React.useMemo(() => (
|
||||
<StoreContext.Provider value={{ state, dispatch }}>
|
||||
<App />
|
||||
</StoreContext.Provider>
|
||||
), [state, dispatch])
|
||||
};
|
||||
```
|
||||
|
||||
#### 任意子组件访问/修改共享状态 - child.ts
|
||||
|
||||
```tsx
|
||||
import { StoreContext } from './store'
|
||||
|
||||
const app: React.FC = () => {
|
||||
const { state, dispatch } = React.useContext(StoreContext);
|
||||
|
||||
return React.useMemo(() => (
|
||||
<div>{state.name}</div>
|
||||
), [state.name])
|
||||
};
|
||||
```
|
||||
|
||||
如上解决了 **多个联系紧密组件模块间便捷共享状态的问题** ,但有时也会遇到需要共享根组件 Props 的问题,**这种不可修改的状态不适合一并塞到 `StoreContext` 里**,我们新建一个 `PropsContext` 注入根组件的 Props:
|
||||
|
||||
```tsx
|
||||
const PropsContext = React.createContext<Props>(null)
|
||||
|
||||
const AppProvider: React.FC<Props> = props => {
|
||||
return React.useMemo(() => (
|
||||
<PropsContext.Provider value={props}>
|
||||
<App />
|
||||
</PropsContext.Provider>
|
||||
), [props])
|
||||
};
|
||||
```
|
||||
|
||||
#### 结合项目数据流
|
||||
|
||||
参考 [react-redux hooks](https://github.com/reduxjs/react-redux/blob/master/docs/api/hooks.md)。
|
||||
|
||||
### debounce 优化
|
||||
|
||||
比如当输入框频繁输入时,为了保证页面流畅,我们会选择在 `onChange` 时进行 `debounce` 。然而在 Function Component 领域中,我们有更优雅的方式实现。
|
||||
|
||||
> 其实在 Input 组件 `onChange` 使用 `debounce` 有一个问题,就是当 Input 组件 **受控** 时, `debounce` 的值不能及时回填,导致甚至无法输入的问题。
|
||||
|
||||
我们站在 Function Component 思维模式下思考这个问题:
|
||||
|
||||
1. React [scheduling](https://github.com/dt-fe/weekly/blob/v2/099.%E7%B2%BE%E8%AF%BB%E3%80%8AScheduling%20in%20React%E3%80%8B.md) 通过智能调度系统优化渲染优先级,我们其实不用担心频繁变更状态会导致性能问题。
|
||||
2. 如果联动一个文本还觉得慢吗? `onChange` 本不慢,大部分使用值的组件也不慢,没有必要从 `onChange` 源头开始就 `debounce` 。
|
||||
3. 找到渲染性能最慢的组件(比如 iframe 组件),**对一些频繁导致其渲染的入参进行 `useDebounce`** 。
|
||||
|
||||
下面是一个性能很差的组件,引用了变化频繁的 `text` (这个 `text` 可能是 `onChange` 触发改变的),我们利用 `useDebounce` 将其变更的频率慢下来即可:
|
||||
|
||||
```typescript
|
||||
const App: React.FC = ({ text }) => {
|
||||
// 无论 text 变化多快,textDebounce 最多 1 秒修改一次
|
||||
const textDebounce = useDebounce(text, 1000)
|
||||
|
||||
return useMemo(() => {
|
||||
// 使用 textDebounce,但渲染速度很慢的一堆代码
|
||||
}, [textDebounce])
|
||||
};
|
||||
```
|
||||
|
||||
使用 `textDebounce` 替代 `text` 可以将渲染频率控制在我们指定的范围内。
|
||||
|
||||
### useEffect 注意事项
|
||||
|
||||
事实上,`useEffect` 是最为怪异的 Hook,也是最难使用的 Hook。比如下面这段代码:
|
||||
|
||||
```tsx
|
||||
useEffect(() => {
|
||||
props.onChange(props.id)
|
||||
}, [props.onChange, props.id])
|
||||
```
|
||||
|
||||
如果 `id` 变化,则调用 `onChange`。但如果上层代码并没有对 `onChange` 进行合理的封装,导致每次刷新引用都会变动,则会产生严重后果。我们假设父级代码是这么写的:
|
||||
|
||||
```tsx
|
||||
class App {
|
||||
render() {
|
||||
return <Child id={this.state.id} onChange={id => this.setState({ id })} />
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
这样会导致死循环。虽然看上去 `<App>` 只是将更新 id 的时机交给了子元素 `<Child>`,但由于 `onChange` 函数在每次渲染时都会重新生成,因此引用总是在变化,就会出现一个无限死循环:
|
||||
|
||||
新 `onChange` -> `useEffect` 依赖更新 -> `props.onChange` -> 父级重渲染 -> 新 `onChange`...
|
||||
|
||||
想要阻止这个循环的发生,只要改为 `onChange={this.handleChange}` 即可,**`useEffect` 对外部依赖苛刻的要求,只有在整体项目都注意保持正确的引用时才能优雅生效。**
|
||||
|
||||
然而被调用处代码怎么写并不受我们控制,这就导致了不规范的父元素可能导致 React Hooks 产生死循环。
|
||||
|
||||
因此在使用 `useEffect` 时要注意调试上下文,注意父级传递的参数引用是否正确,如果引用传递不正确,有两种做法:
|
||||
|
||||
1. 使用 [useDeepCompareEffect](https://github.com/streamich/react-use/blob/master/docs/useDeepCompareEffect.md) 对依赖进行深比较。
|
||||
2. 使用 `useCurrentValue` 对引用总是变化的 props 进行包装:
|
||||
|
||||
```tsx
|
||||
function useCurrentValue<T>(value: T): React.RefObject<T> {
|
||||
const ref = React.useRef(null);
|
||||
ref.current = value;
|
||||
return ref;
|
||||
}
|
||||
|
||||
const App: React.FC = ({ onChange }) => {
|
||||
const onChangeCurrent = useCurrentValue(onChange)
|
||||
};
|
||||
```
|
||||
|
||||
`onChangeCurrent` 的引用保持不变,但每次都会指向最新的 `props.onChange`,从而可以规避这个问题。
|
||||
|
||||
## 总结
|
||||
|
||||
如果还有补充,欢迎在文末讨论。
|
||||
|
||||
如需了解 Function Component 或 Hooks 基础用法,可以参考往期精读:
|
||||
|
||||
- [精读《React Hooks》](https://github.com/dt-fe/weekly/blob/v2/079.%E7%B2%BE%E8%AF%BB%E3%80%8AReact%20Hooks%E3%80%8B.md)
|
||||
- [精读《怎么用 React Hooks 造轮子》](https://github.com/dt-fe/weekly/blob/v2/080.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%80%8E%E4%B9%88%E7%94%A8%20React%20Hooks%20%E9%80%A0%E8%BD%AE%E5%AD%90%E3%80%8B.md)
|
||||
- [精读《useEffect 完全指南》](https://github.com/dt-fe/weekly/blob/v2/096.%E7%B2%BE%E8%AF%BB%E3%80%8AuseEffect%20%E5%AE%8C%E5%85%A8%E6%8C%87%E5%8D%97%E3%80%8B.md)
|
||||
- [精读《Function Component 入门》](https://github.com/dt-fe/weekly/blob/v2/104.精读《Function%20Component%20入门》.md)
|
||||
|
||||
> 讨论地址是:[精读《React Hooks 最佳实践》 · Issue #202 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/202)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
|
||||
|
||||
> 关注 **前端精读微信公众号**
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
|
||||
|
||||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
|
||||
@@ -0,0 +1,175 @@
|
||||
## 简介
|
||||
|
||||
商业智能(Business Intelligence)简称 BI,即通过数据挖掘与分析找到商业洞察,助力商业成功。
|
||||
|
||||
一个完整的 BI 链路包含数据采集、数据清洗、数据挖掘、数据展现,其本质是对数据进行多维分析。前端的主要工作在数据展现环节,由于展示方式繁多、分析模型复杂且数据量大,前端环节的复杂度很高。
|
||||
|
||||
在 BI 做前端非常有挑战,开发者需要充分理解数据概念,而本身复杂度较高的可视化建站也只是 BI 的基础能力,想要建设 BI 的上层能力,比如探索式分析和数据洞察,都需要在前后端引入更复杂的计算模型。
|
||||
|
||||
本文作为一个引子,简单介绍笔者做 BI 的经验,后面如果有机会再写一个系列文章对细节进行阐述。
|
||||
|
||||
## 精读
|
||||
|
||||
国内目前处于 BI 1.0 阶段,也就是报表阶段,因此笔者将阐述这个阶段 BI 的核心开发概念。
|
||||
|
||||
> BI 2.0 探索式分析阶段是国内数据分析最前沿领域,这部分等开发完成后再分享。
|
||||
|
||||
BI 1.0 阶段的核心概念包括 **数据集、渲染引擎、数据模型、可视化** 这四个技术模块。
|
||||
|
||||
### 数据集
|
||||
|
||||
数据集即数据的集合,在 BI 领域更多指一种标准化的数据结构。
|
||||
|
||||
任何数据都可以封装成数据集,比如 txt 文本、excel、mysql 数据库等等。
|
||||
|
||||
数据集的基本形态是二维表格,列头表示字段,每一行就是一份数据,数据展示就是通过对这些数据字段进行多维度分析。
|
||||
|
||||
#### 数据集导入
|
||||
|
||||
一般来说数据集导入有两种方式,分别是本地文件上传与数据库链接。本地文件上传又分为多种文件类型处理,比如对 excel 的解析,可能还包括数据清洗;数据库链接分析可视化导入与 SQL 输入。
|
||||
|
||||
可视化导入需要提前对数据库进行结构分析,绘制出表结构与字段结构,不用理解 SQL 也可以进行可视化操作。
|
||||
|
||||
SQL 输入可以利用 [monaco-editor](https://github.com/microsoft/monaco-editor) 等 web 代码编辑器作为输入框,最好能结合智能提示提高 sql 编写效率。sql 智能提示可以参考往期精读 [精读《手写 SQL 编译器 - 智能提示》](https://github.com/dt-fe/weekly/blob/v2/085.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%89%8B%E5%86%99%20SQL%20%E7%BC%96%E8%AF%91%E5%99%A8%20-%20%E6%99%BA%E8%83%BD%E6%8F%90%E7%A4%BA%E3%80%8B.md)。
|
||||
|
||||
#### 数据集建模
|
||||
|
||||
数据集建模一般包含 **维度度量建模、字段配置、层系建模**。
|
||||
|
||||
维度度量建模需要智能分析出字段属于维度还是度量,一般会结合字段实际的值或者字段名来智能判断字段类型,如果数据库信息中已存储了字段类型,就可以 100% 准确归类。
|
||||
|
||||
字段配置即对字段进行增删或修改,还可以新增聚合字段或对比字段。
|
||||
|
||||
聚合字段是指将一个字段表达式封装为一个新字段,这里也会用到一个简单的 sql 编辑器,只需要支持四则运算、字段提示、以及一些基本函数的组合即可。
|
||||
|
||||
对比字段是指新增的字段是基于已有字段在某个时间周期内的对比,比如对 UV 字段的年同比就可以封装为一个对比字段。对比字段在前端技术上没有什么难度,仅需理解概念即可。
|
||||
|
||||
### 渲染引擎
|
||||
|
||||
渲染引擎包括了对报表进行编辑与渲染的引擎,理论上可以合二为一。
|
||||
|
||||
渲染引擎的重要模块包括:画布拖拽、组件编辑、事件中心。
|
||||
|
||||
画布拖拽其实包含了组件自定义开发流程,到 CDN 发布、CDN 加载、组件拖拽、画布排版等一系列技术点,每个点展开都有写不完的细节,但好在这套功能属于通用建站基础功能点,本文就不再赘述。
|
||||
|
||||
组件编辑中,基本属性的编辑与属于通用建站领域的表单模型范畴,一般通过 UISchema 来描述通用表单,这块也不再赘述。组件编辑的另一部分就是数据编辑,这部分在后面数据模型章节里详细讲。
|
||||
|
||||
事件中心是渲染引擎部分,此功能在编辑状态需要禁用。这个功能可以实现图表联动、上卷下钻等数据能力。一个通用事件中心一般包括 **事件触发** 与 **事件响应** 两部分,基本结构如下:
|
||||
|
||||
```typescript
|
||||
interface Event {
|
||||
trigger:
|
||||
| {
|
||||
type: 'callback';
|
||||
callbackName: string;
|
||||
}
|
||||
| {
|
||||
type: 'listener';
|
||||
eventName: string;
|
||||
}
|
||||
| {
|
||||
type: 'system';
|
||||
name: string;
|
||||
};
|
||||
|
||||
action:
|
||||
| {
|
||||
type: 'dispatch';
|
||||
eventName: string;
|
||||
}
|
||||
| {
|
||||
type: 'jumpUrl';
|
||||
url: string;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`trigger` 即事件触发,包括基本的系统事件 `system`,比如定时器或者初始化自动触发;组件的回调 `callback` 比如当按钮被点击时;事件监听 `listener` 比如另一个事件被触发时,这个事件可能来自于 `action`。
|
||||
|
||||
`action` 即事件响应,包括基本的事件触发 `dispatch`,可以触发其他事件,可以构成一个事件链路;其他的 `action` 就是数据相关,可以用来做条件联动、字段联动、数据集联动等等,因为实现各异这里不做介绍。
|
||||
|
||||
事件机制还需要支持值传递,即事件触发源的值可以传递到事件响应方。值传递可以在触发源内部进行,比如当触发源是回调函数时,函数参数就自然作为值传递过去,触发源通过 `...args` 方式接收。
|
||||
|
||||
#### 数据钻取
|
||||
|
||||
配置了层系的字段都可以进行数据钻取。层系可以在数据集配置,也可以在报表编辑页配置,可以理解为一个顺序有关的文件夹,将文件夹作为字段使用时,默认生效的是第一个子元素,之后可以按照顺序分别进行下钻。
|
||||
|
||||
比如 “地区” 层系包含了国家、省、市、区,那么就可以按照这个层级进行数据上卷下钻。
|
||||
|
||||
如果一个字段是层系字段,图表需要有对应的操作区域进行上卷下钻,数据编辑区域也可以进行同样操作。数据钻取的计算过程不在图表内部处理,而是触发一个状态后,由渲染引擎将这个层系字段实例状态改为下钻到第 N 层,并且每下钻一次就多拿到一列的数据,由图表组件进行下钻展示。
|
||||
|
||||
一般来说下钻后数据仍是全量的,有时候为了避免数据量过大,比如在柱状图点击某个柱子进行下钻,只想看这个柱子下钻后的数据:比如 2017、2018、2019 年三年的数据,下钻到月后数据量是 3 x 12 = 36 条,但如果仅在 2019 年进行下钻,只想看 2019 年的 12 条数据,可以转化为下钻 + 筛选条件的模式:全局下钻展开后 36 条,在 2019 年上点击下钻后,增加一个筛选条件(年 = 2019),这样就达到了效果,整个流程对图表组件是无感知的。
|
||||
|
||||
### 数据模型
|
||||
|
||||
与通用表单模型 UISchema 相对应,数据模型笔者称之为 CubeSchema,因为 BI 领域对数据的多维处理模型成为 Cube 立方体,数据配置即表示如何对这个立方体进行查询,因此其配置表单成为 CubeSchema。
|
||||
|
||||
不管是探索式分析还是 BI 1.0 的报表阶段,数据模型的基本概念是通用的(探索式分析固定了行列,且增加了标记):将字段放置到不同的区域,这些区域的划分方式可以按照功能:横轴、纵轴;按照概念:维度、度量;按照探索分析思路:固化为行、列等等。
|
||||
|
||||
这块可能涉及到的技术点有:拖拽、批量选择+拖拽、双击后按照维度度量自动添加、图表切换后区域字段自动迁移、对字段拖拽的系列配置:限制数量、限制类型、限制数据集、是否重复等等。
|
||||
|
||||
拖拽可以用 [react-beautiful-dnd](https://github.com/atlassian/react-beautiful-dnd) 等库,与渲染引擎拖拽方案基本类似,遇到有层系的数据集还需支持嵌套层级的拖拽。
|
||||
|
||||
图表切换后字段迁移,可以将每个拖拽区域设置若干类型:
|
||||
|
||||
```json
|
||||
{
|
||||
"dataType": ["dimension"]
|
||||
}
|
||||
```
|
||||
|
||||
这样在切换后,维度类型的字段可以自动迁移到维度类型区域,如果对应区域字段数量达到了 `limit` 限制,就继续填充到下一个区域,直到字段用尽或区域填充完为止。
|
||||
|
||||
如果在探索式分析场景里,需要提前对字段进行维度度量建模,在切换时按照图表情况进行相应的处理。比如折线图切换到表格的情况:折线图是天然一个维度(主轴) + N 个度量的场景,表格是天然两个维度(行、列)+ 1 个度量的场景(也可以支持多个,对单元格进行再切分即可),那么从折线图切换到表格时,度量就会落到标记的文本区域;如果从拥有行和列的表格切换到柱状图(之所以无法切换到折线图,是因为表格的度量值一般是离散的,而折线图度量值一般是连续的),表格的行与列的字段会落到柱状图的维度轴,表现效果是对维度轴进行下钻。
|
||||
|
||||
> [精读《Tableau 探索式模型》](https://github.com/dt-fe/weekly/blob/v2/117.%E7%B2%BE%E8%AF%BB%E3%80%8ATableau%20%E6%8E%A2%E7%B4%A2%E5%BC%8F%E6%A8%A1%E5%9E%8B%E3%80%8B.md) 了解更多探索式分析。
|
||||
|
||||
数据模型还包括数据分析相关配置,比如设置对比字段,或者均值线等分析功能。这些数据计算工作放在后端,前端需要将配置项整理到取数接口中,并按照数据驱动的方式展现。
|
||||
|
||||
对于对比字段等 “拓展字段” 的分析功能,可以拓展通用取数接口,图表组件无感知,相当于多添加了几个隐藏字段;去特殊值等对标准数据进行操作的情况图表组件也无需感知。
|
||||
|
||||
聚类、均值线等需要图表组件额外展示的部分抽象为一套固定的数据格式透传给图表组件,由图表组件自行处理。
|
||||
|
||||
可以看出来,都是取数 + 展示,普通的前端业务与 BI 业务开发的区别:
|
||||
|
||||
普通前端业务是以业务逻辑为核心的,根据业务需要确定接口格式;BI 业务是以数据为核心的,围绕数据计算模型确定一套固定的接口格式,取数不依赖组件,所有组件对标准数据都有对应的展现。
|
||||
|
||||
### 可视化
|
||||
|
||||
与普通可视化组件不同,BI 可视化组件需要对接 CubeSchema 模型,同时还要支持 **大数据性能优化、边界数据展示优化、交互响应**。
|
||||
|
||||
对接 CubeSchema 即统一对接二维表格的数据,大部分组件都是二维以上结构展示,因此对接起来并不困难,有一些一维数据结构的组件比如单指标块就要舍弃其中的某一维,需要确定一套规则。
|
||||
|
||||
二维以上部分是较为通用的,虽然计算模型是基于 Cube N 维的,但组件可以通过标准轴进行多维度展开,或者说下钻来实现类似效果。对于折线图来说,轴的含义有限,可以用分面的方式展示多维数据。当然也有一些组件只适合展示特定维度数量的数据。
|
||||
|
||||
#### 大数据性能优化
|
||||
|
||||
可视化组件特别需要关注性能优化,因为 BI 查询出的数据量可能非常大,特别是多层下钻或基于地理的数据。
|
||||
|
||||
技术手段包括 GPU 渲染、缓存 canvas、多线程运算等,业务手段包括数据抽样、按需渲染可视区域、限制数据条数等等。
|
||||
|
||||
#### 边界数据展示优化
|
||||
|
||||
永远不知道数据集会给出怎样的数据,因此 BI 边界情况特别多,可能点非常密集,也可能丢失一些数据导致渲染异常。图表组件需要利用避让算法将密集的数据打散或着色,目的是为了容易阅读,对于丢失的异常数据也要有保护性的补全机制。
|
||||
|
||||
#### 交互响应
|
||||
|
||||
包括上卷下钻、点选、圈选、高亮等交互操作,这些操作反馈到渲染引擎导致数据变化并将新的数据灌入图表组件。
|
||||
|
||||
业务逻辑上这些交互操作并不复杂,难点在使用的可视化库是否有这个能力,以及如何统一交互行为。
|
||||
|
||||
## 总结
|
||||
|
||||
BI 领域的四大方向:数据集、渲染引擎、数据模型与可视化都有许多可以做深的技术点,每一块都需要深入沉淀几年技术经验才能做好,需要大量优秀人才通力协作才有可能做好。
|
||||
|
||||
目前我们在阿里数据中台正在打造一款面向未来的优秀 BI 工具,如果 BI 领域让你觉得有挑战,随时欢迎你的加入,联系邮箱:ziyi.hzy@alibaba-inc.com
|
||||
|
||||
> 讨论地址是:[精读《前端与 BI》 · Issue #208 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/208)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
|
||||
|
||||
> 关注 **前端精读微信公众号**
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
|
||||
|
||||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
|
||||
@@ -0,0 +1,486 @@
|
||||
## 1 引言
|
||||
|
||||
在写这次精读之前,我想谈谈前端精读可以为读者带来哪些价值,以及如何评判这些价值。
|
||||
|
||||
前端精读已经写到第 123 篇了,大家已经不必担心它突然停止更新,因为我已养成每周写一篇文章的习惯,而读者也养成了每周看一篇的习惯。所以我想说的其实是一种更有生命力的自媒体运作方式,定期更新。一个定期更新的专栏比一个不不定期更新的专栏更有活力,也更受读者喜爱,因为读者能看到文章之间的联系,跟随作者一起成长。个人学习也是如此,养成定期学习的习惯,比在培训班突击几个月更有用,学会在生活中规律的学习,甚至好过读几年名牌大学。
|
||||
|
||||
前端精读想带给读者的不仅是一篇篇具体的内容和知识,知识是无穷无尽的,几万篇文章也说不完,但前端精读一直沿用了“引言-概述-精读-总结”这套学习模式,无论是前端任何领域的问题,还是对人生和世界的思考都可以套用,希望能为读者提供一套学习思维框架,让你能学习到如何找到好的文章,以及如何解读它。
|
||||
|
||||
至今已经选择了许多源码解读的题材,与培训思维的源码解读不同,我希望你不要带着面试的目的学习源码,因为这样会让你只局限在 react、vue 这种热门的框架上。前端精读选取的框架类型之所以广泛,是希望你能静下心来,吸取不同框架风格与作者的优势,培养一种优雅编码的气质。
|
||||
|
||||
进入正题,这次选择的文章 [《用 Babel 创造自定义 JS 语法》](https://lihautan.com/creating-custom-javascript-syntax-with-babel/) 也是培养编码气质的一类文章,虽然对你实际工作用处不大,但这篇文章可以培养几个程序员梦寐以求的能力:深入理解 Babel、深入理解框架拓展机制。理解一个复杂系统或培养框架思维不是一朝一夕的,但持续阅读这种文章可以让你越来越接近掌握它。
|
||||
|
||||
之所以选择 Babel,是因为 Babel 处理的一直是语法树相关的底层逻辑,编译原理是程序世界的基座之一,拥有很大的学习价值。所以我们的目的并不是像文章标题说的 - 创造一个自定义 JS 语法,因为你创造的语法只会让 JS 复杂体系更加混乱,但可以让你理解 Babel 解析标准 JS 语法的原理,以及看待新语法提案时,拥有从实现层面思考的能力。
|
||||
|
||||
最后,不必多说,能重温 Babel 经典的插件机制,你可以发现 Babel 的插件拓展机制和 Antrl4 很像,在设计业务模块拓展方案时也可以作为参考。
|
||||
|
||||
## 2 概述
|
||||
|
||||
我们要利用 Babel 实现 `function @@` 的新语法,用 `@@` 装饰的函数会自动柯里化:
|
||||
|
||||
```js
|
||||
// '@@' makes the function `foo` curried
|
||||
function @@ foo(a, b, c) {
|
||||
return a + b + c;
|
||||
}
|
||||
console.log(foo(1, 2)(3)); // 6
|
||||
```
|
||||
|
||||
可以看到,`function @@ foo` 描述的函数 `foo` 支持 `foo(1, 2)(3)` 这种柯里化调用。
|
||||
|
||||
实现方式分为两步:
|
||||
|
||||
1. Fork babel 源码。
|
||||
2. 创建一个 babel 转换器插件。
|
||||
|
||||
不要畏惧这些步骤,“如果你读完了这篇文章,你将成为同事眼中的 Babel 大神” - 原文。
|
||||
|
||||
首先 Fork babel 源码到本地,执行下面的命令可以初始化并编译 babel:
|
||||
|
||||
```bash
|
||||
$ make bootstrap
|
||||
$ make build
|
||||
```
|
||||
|
||||
babel 使用 [Makefile](https://opensource.com/article/18/8/what-how-makefile) 执行编译命令,并且采用 monorepo 管理,我们这次要关心的是 `package/babel-parser` 这个模块。
|
||||
|
||||
### 词法
|
||||
|
||||
首先要了解词法知识,更详细的可以阅读原文或精读之前的一篇系列文章:[精读《词法分析》](https://github.com/dt-fe/weekly/blob/v2/064.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%89%8B%E5%86%99%20SQL%20%E7%BC%96%E8%AF%91%E5%99%A8%20-%20%E8%AF%8D%E6%B3%95%E5%88%86%E6%9E%90%E3%80%8B.md)。
|
||||
|
||||
要解析语法,首先要进行词法分析。任何语法输入都是一个字符串,比如 `function @@ foo(a, b, c)`,词法分析就是要将这个长度为 24 的字符拆分为一个个有语义的单词片段:`function` `@@` `foo` `(` `a` ..
|
||||
|
||||
由于 `@@` 是我们创造的语法,所以我们第一个任务就是让 babel 词法分析可以识别它。
|
||||
|
||||
下面是 `package/babel-parser` 的文件结构:
|
||||
|
||||
```text
|
||||
- src/
|
||||
- tokenizer/
|
||||
- parser/
|
||||
- plugins/
|
||||
- jsx/
|
||||
- typescript/
|
||||
- flow/
|
||||
- ...
|
||||
- test/
|
||||
```
|
||||
|
||||
可以看到,分为词法分析 `tokenizer`,语法分析 `parser`,以及支持一些特殊语法的插件,以及测试用例 `test`。
|
||||
|
||||
推荐使用 **Test-driven development (TDD) - 测试驱动开发的方式**,就是先写测试用例,再根据测试用例开发。这种开发方式在后端或者 babel 这种底层框架很常见,因为 TDD 方式开发的逻辑能保证测试用例 100% 覆盖,同时先看测试用例也是个很好的切面编程思维。
|
||||
|
||||
```js
|
||||
// packages/babel-parser/test/curry-function.js
|
||||
|
||||
import { parse } from '../lib';
|
||||
|
||||
function getParser(code) {
|
||||
return () => parse(code, { sourceType: 'module' });
|
||||
}
|
||||
|
||||
describe('curry function syntax', function() {
|
||||
it('should parse', function() {
|
||||
expect(getParser(`function @@ foo() {}`)()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
可以利用 jest 直接测试这段代码:
|
||||
|
||||
```bash
|
||||
BABEL_ENV=test node_modules/.bin/jest -u packages/babel-parser/test/c
|
||||
```
|
||||
|
||||
结果会出现如下报错:
|
||||
|
||||
```text
|
||||
SyntaxError: Unexpected token (1:9)
|
||||
|
||||
at Parser.raise (packages/babel-parser/src/parser/location.js:39:63)
|
||||
at Parser.raise [as unexpected] (packages/babel-parser/src/parser/util.js:133:16)
|
||||
at Parser.unexpected [as parseIdentifierName] (packages/babel-parser/src/parser/expression.js:2090:18)
|
||||
at Parser.parseIdentifierName [as parseIdentifier] (packages/babel-parser/src/parser/expression.js:2052:23)
|
||||
at Parser.parseIdentifier (packages/babel-pars
|
||||
```
|
||||
|
||||
第 9 个字符就是 `@`,说明程序现在还不支持函数前面的 `@` 解析。我们还可以在错误堆栈中找到报错位置,并把当前 Token 与下一个 Token 打印出来:
|
||||
|
||||
```js
|
||||
// packages/babel-parser/src/parser/expression.js
|
||||
|
||||
parseIdentifierName(pos: number, liberal?: boolean): string {
|
||||
if (this.match(tt.name)) {
|
||||
// ...
|
||||
} else {
|
||||
console.log(this.state.type); // current token
|
||||
console.log(this.lookahead().type); // next token
|
||||
throw this.unexpected();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`this.state.type` 代表当前 Token,`this.lookahead().type` 表示下一个 Token。`lookahead` 是词法分析的专有词,表示向后查看。打印之后,我们会发现输出了两个 `@` Token:
|
||||
|
||||
```js
|
||||
TokenType {
|
||||
label: '@',
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
下一步,我们需要让 babel 词法分析识别 `@@` 这个 Token。首先需要注册这个 Token:
|
||||
|
||||
```js
|
||||
// packages/babel-parser/src/tokenizer/types.js
|
||||
|
||||
export const types: { [name: string]: TokenType } = {
|
||||
// ...
|
||||
at: new TokenType('@'),
|
||||
atat: new TokenType('@@'),
|
||||
};
|
||||
```
|
||||
|
||||
注册了之后,我们要在遍历 Token 时增加判断 “如果当前字符是 `@` 且下一个字符也是 `@`,则整体构成了 `@@` Token 并且光标向后移动两格”:
|
||||
|
||||
```js
|
||||
// packages/babel-parser/src/tokenizer/index.js
|
||||
|
||||
getTokenFromCode(code: number): void {
|
||||
switch (code) {
|
||||
// ...
|
||||
case charCodes.atSign:
|
||||
// if the next character is a `@`
|
||||
if (this.input.charCodeAt(this.state.pos + 1) === charCodes.atSign) {
|
||||
// create `tt.atat` instead
|
||||
this.finishOp(tt.atat, 2);
|
||||
} else {
|
||||
this.finishOp(tt.at, 1);
|
||||
}
|
||||
return;
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
再次运行测试文件,输出变成了:
|
||||
|
||||
```js
|
||||
// current token
|
||||
TokenType {
|
||||
label: '@@',
|
||||
// ...
|
||||
}
|
||||
|
||||
// next token
|
||||
TokenType {
|
||||
label: 'name',
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
到这一步,已经能正确解析 `@@` Token 了。
|
||||
|
||||
## 语法
|
||||
|
||||
词法已经可以将 `@@` 解析为 `atat` Token,下一步我们就要利用这个 Token,让生成的 AST 结构中包含柯里化函数的信息,并利用 babel 插件在解析时实现柯里化功能。
|
||||
|
||||
首先我们可以在 [Babel AST explorer](https://lihautan.com/babel-ast-explorer/#?eyJiYWJlbFNldHRpbmdzIjp7InZlcnNpb24iOiI3LjYuMCJ9LCJ0cmVlU2V0dGluZ3MiOnsiaGlkZUVtcHR5Ijp0cnVlLCJoaWRlTG9jYXRpb24iOnRydWUsImhpZGVUeXBlIjp0cnVlfSwiY29kZSI6ImZ1bmN0aW9uICogZm9vKCkge30ifQ==) 看到 AST 解析的结构,我们拿 generator 函数测试,因为这个函数结构与柯里化函数类似:
|
||||
|
||||

|
||||
|
||||
可以看到,babel 通过 `generator` `async` 属性来标识函数是否为 generator 或者 async 函数。同理,增加一个 `curry` 属性就可以实现第一步了:
|
||||
|
||||

|
||||
|
||||
要实现如上效果,只需在词法分析 `parser/statement` 文件的 `parseFunction` 处新增 `atat` 解析即可:
|
||||
|
||||
```js
|
||||
// packages/babel-parser/src/parser/statement.js
|
||||
|
||||
export default class StatementParser extends ExpressionParser {
|
||||
// ...
|
||||
parseFunction<T: N.NormalFunction>(
|
||||
node: T,
|
||||
statement?: number = FUNC_NO_FLAGS,
|
||||
isAsync?: boolean = false
|
||||
): T {
|
||||
// ...
|
||||
node.generator = this.eat(tt.star);
|
||||
node.curry = this.eat(tt.atat);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`eat` 是吃掉的意思,实际上可以理解为吞掉这个 Token,这样做有两个效果:1. 为函数添加了 `curry` 属性 2. 吞掉了 `@@` 标识,保证所有 Token 都被识别是 AST 解析正确的必要条件。
|
||||
|
||||
关于递归下降语法分析的更多知识,可以参考 [精读《手写 SQL 编译器 - 语法分析》](https://github.com/dt-fe/weekly/blob/v2/066.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%89%8B%E5%86%99%20SQL%20%E7%BC%96%E8%AF%91%E5%99%A8%20-%20%E8%AF%AD%E6%B3%95%E5%88%86%E6%9E%90%E3%80%8B.md),或者阅读原文。
|
||||
|
||||
我们再次执行测试函数,发现测试通过了,一切都在预料中。
|
||||
|
||||
## babel 插件
|
||||
|
||||
现在我们得到了标记了 `curry` 的 AST,那么最后需要一个 babel 解析插件,实现柯里化。
|
||||
|
||||
首先我们通过修改 babel 源码的方式实现的效果,是可以转化为自定义 babel parser 插件的:
|
||||
|
||||
```js
|
||||
// babel-plugin-transformation-curry-function.js
|
||||
|
||||
import customParser from './custom-parser';
|
||||
|
||||
export default function ourBabelPlugin() {
|
||||
return {
|
||||
parserOverride(code, opts) {
|
||||
return customParser.parse(code, opts);
|
||||
},
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
这样就可以实现修改 babel 源码一样的效果,这也是做框架常用的插件机制。
|
||||
|
||||
其次我们要理解如何实现柯里化。柯里化可以通过柯里函数包装后实现:
|
||||
|
||||
```js
|
||||
function currying(fn) {
|
||||
const numParamsRequired = fn.length;
|
||||
function curryFactory(params) {
|
||||
return function (...args) {
|
||||
const newParams = params.concat(args);
|
||||
if (newParams.length >= numParamsRequired) {
|
||||
return fn(...newParams);
|
||||
}
|
||||
return curryFactory(newParams);
|
||||
}
|
||||
}
|
||||
return curryFactory([]);
|
||||
}
|
||||
|
||||
// from
|
||||
function @@ foo(a, b, c) {
|
||||
return a + b + c;
|
||||
}
|
||||
|
||||
// to
|
||||
const foo = currying(function foo(a, b, c) {
|
||||
return a + b + c;
|
||||
})
|
||||
```
|
||||
|
||||
柯里化函数通过构造参数数量相关的递归,当参数传入不足时返回一个新函数,并持久化之前传入的参数,最后当参数齐全后一次性调用函数。
|
||||
|
||||
我们需要做的是,将 `@@ foo` 解析为 `currying()` 函数包裹后的新函数。
|
||||
|
||||
下面就是我们熟悉的 babel 插件部分了:
|
||||
|
||||
```js
|
||||
// babel-plugin-transformation-curry-function.js
|
||||
|
||||
export default function ourBabelPlugin() {
|
||||
return {
|
||||
// ...
|
||||
visitor: {
|
||||
FunctionDeclaration(path) {
|
||||
if (path.get('curry').node) {
|
||||
// const foo = curry(function () { ... });
|
||||
path.node.curry = false;
|
||||
path.replaceWith(
|
||||
t.variableDeclaration('const', [
|
||||
t.variableDeclarator(
|
||||
t.identifier(path.get('id.name').node),
|
||||
t.callExpression(t.identifier('currying'), [
|
||||
t.toExpression(path.node),
|
||||
])
|
||||
),
|
||||
])
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
`FunctionDeclaration` 就是 AST 的 visit 钩子,这个钩子在执行到函数时被触发,我们通过 `path.get('curry')` 拿到 **柯里化函数**,并利用 `replaceWith` 将这个函数构造为一个被 `currying` 函数包裹的新函数。
|
||||
|
||||
剩下最后一个问题:`currying` 函数源码放在哪里。
|
||||
|
||||
第一种方式,创建类似 `babel-plugin-transformation-curry-function` 这样的插件,在 babel 解析时将 `currying` 函数注册到全局,这是全局思维的方案。
|
||||
|
||||
第二种是模块化解决方案,创建一个自定义的 `@babel/helpers`,注册一个 `currying` 标识:
|
||||
|
||||
```js
|
||||
// packages/babel-helpers/src/helpers.js
|
||||
helpers.currying = helper("7.6.0")`
|
||||
export default function currying(fn) {
|
||||
const numParamsRequired = fn.length;
|
||||
function curryFactory(params) {
|
||||
return function (...args) {
|
||||
const newParams = params.concat(args);
|
||||
if (newParams.length >= numParamsRequired) {
|
||||
return fn(...newParams);
|
||||
}
|
||||
return curryFactory(newParams);
|
||||
}
|
||||
}
|
||||
return curryFactory([]);
|
||||
}
|
||||
`;
|
||||
```
|
||||
|
||||
在 visit 函数使用 `addHelper` 方式拿到 `currying`:
|
||||
|
||||
```js
|
||||
path.replaceWith(
|
||||
t.variableDeclaration('const', [
|
||||
t.variableDeclarator(
|
||||
t.identifier(path.get('id.name').node),
|
||||
t.callExpression(this.addHelper("currying"), [
|
||||
t.toExpression(path.node),
|
||||
])
|
||||
),
|
||||
])
|
||||
);
|
||||
```
|
||||
|
||||
这样在 babel 转换后,就会自动 import helper,并引用 helper 中导出的 `currying`。
|
||||
|
||||
最后原文末尾留下了一些延伸阅读内容,感兴趣的同学可以 [点击到原文](https://lihautan.com/creating-custom-javascript-syntax-with-babel/)。
|
||||
|
||||
## 3 精读
|
||||
|
||||
读完这篇文章,相信你不仅对 babel 插件有了更深刻的认识,而且还掌握了如何为 js 添加新语法这种黑魔法。
|
||||
|
||||
我来帮你从 babel 这篇文章总结一些编程模型和知识点,借助 babel 创造自定义语法的实例,加深对它们的理解。
|
||||
|
||||
### TDD
|
||||
|
||||
Test-driven development 即测试驱动的开发模式。
|
||||
|
||||
从文章的例子可以看出,创造一个新语法,可以先在测试用例先写上这个语法,通过执行测试命令通过报错堆栈一步步解决问题。这种方式开发可以让测试覆盖率更高,目的更专注,更容易保障代码质量。
|
||||
|
||||
### 联想编程
|
||||
|
||||
联想编程不属于任何编程模型,但从简介的思路来看,作者把 “为 babel 创建一个新 js 语法” 看作一种探案式探索过程,通过错误堆栈和代码阅读,一步一步通过合理联想实现最终目的。
|
||||
|
||||
在 AST 那一节,还借助了 [Babel AST explorer](https://lihautan.com/babel-ast-explorer/#?eyJiYWJlbFNldHRpbmdzIjp7InZlcnNpb24iOiI3LjYuMCJ9LCJ0cmVlU2V0dGluZ3MiOnsiaGlkZUVtcHR5Ijp0cnVlLCJoaWRlTG9jYXRpb24iOnRydWUsImhpZGVUeXBlIjp0cnVlfSwiY29kZSI6ImZ1bmN0aW9uICogZm9vKCkge30ifQ==) 工具查看 AST 结构,通过联想到 generator 函数找到类似的 AST 结构,并找到拓展 AST 的突破口。
|
||||
|
||||
随着解决问题的不同,联想方式也不同,如果能够举一反三,对不同场景都能合理的联想,才算是具备了技术专家的软素质。
|
||||
|
||||
### 词法、语法分析
|
||||
|
||||
词法、语法分析属于编译原理的知识,理解词法拆分、递归下降,可以帮助你技术走的更深。
|
||||
|
||||
不论是 Babel 插件的使用、还是 Babel 增加自定义 JS 语法,都要具备基本编译原理知识。编译原理知识还能帮助你开发在线编辑器,做智能语法提示等等。
|
||||
|
||||
### 插件机制
|
||||
|
||||
如下是 babel 自定义 parser 的插件拓展方式:
|
||||
|
||||
```js
|
||||
export default function ourBabelPlugin() {
|
||||
return {
|
||||
parserOverride(code, opts) {
|
||||
return customParser.parse(code, opts);
|
||||
},
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
这只是插件拓展的一种,有申明式,也有命令式;有用 JS 书写的,也有用 JSON 书写的。babel 选择了通过对象方式拓展,是比较适合对 AST 结构统一处理的。
|
||||
|
||||
做框架首先要确定接口规范,比如 parser,先按照接口规范实现一套官方解析,对接时按照接口进行对接,就可以自然而然被用户自定义插件替代了。
|
||||
|
||||
可以参考的文章: [精读《插件化思维》](https://github.com/dt-fe/weekly/blob/v2/053.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%8F%92%E4%BB%B6%E5%8C%96%E6%80%9D%E7%BB%B4%E3%80%8B.md)
|
||||
|
||||
### 柯里化
|
||||
|
||||
柯里化是面试经常考察的一个知识点,我们能学到的有两点:理解递归、理解如何将函数变成柯里化。
|
||||
|
||||
这里再拓展一下,我们还可以想到 JS 尾递归优化。如何快速写一个支持尾递归的函数?
|
||||
|
||||
```js
|
||||
const fn = tailCallOptimize(() => {
|
||||
if ( /* xxx */ ) {
|
||||
fn()
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
通过封装 `tailCallOptimize` 函数,可以很方便的构造一个支持尾递归的函数,这个函数可以这么写:
|
||||
|
||||
```js
|
||||
export function tailCallOptimize<T>(f: T): T {
|
||||
let value: any;
|
||||
let active = false;
|
||||
const accumulated: any[] = [];
|
||||
return function accumulator(this: any) {
|
||||
accumulated.push(arguments);
|
||||
if (!active) {
|
||||
active = true;
|
||||
while (accumulated.length) {
|
||||
value = (f as any).apply(this, accumulated.shift());
|
||||
}
|
||||
active = false;
|
||||
return value;
|
||||
}
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
感兴趣的读者可以在评论里解释一下这个函数的原理。
|
||||
|
||||
### AST visit
|
||||
|
||||
遍历 AST 树常采用的方案是做一个遍历器 visitor,所以在遍历过程中进行拓展常采用 babel 这种方式:
|
||||
|
||||
```js
|
||||
return {
|
||||
// ...
|
||||
visitor: {
|
||||
FunctionDeclaration(path) {
|
||||
if (path.get('curry').node) {
|
||||
// const foo = curry(function () { ... });
|
||||
path.node.curry = false;
|
||||
path.replaceWith(
|
||||
t.variableDeclaration('const', [
|
||||
t.variableDeclarator(
|
||||
t.identifier(path.get('id.name').node),
|
||||
t.callExpression(t.identifier('currying'), [
|
||||
t.toExpression(path.node),
|
||||
])
|
||||
),
|
||||
])
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
`visitor` 下每一个 key 名都是遍历过程中的拓展点,比如上面的例子,我们可以对函数定义位置进行拓展和改写。
|
||||
|
||||
### 内置函数注册
|
||||
|
||||
babel 提供了两种内置函数注册方式,一种类似 polyfill,在全局注册 window 级的变量,另一种是模块化的方式。
|
||||
|
||||
除此之外,可以学习的是 babel 通过 `this.addHelper("currying")` 这种插件拓展方式,在编译后会自动从 helper 引入对应的模块,前提是 `@babel/helper` 需要注册 `currying` 这个 helper。
|
||||
|
||||
babel 将编译过程隐藏了起来,通过一些高度封装的函数调用,以较为语义化方式书写插件,这样写出来的代码也容易理解。
|
||||
|
||||
## 4 总结
|
||||
|
||||
《用 Babel 创造自定义 JS 语法》这篇文章虽然说的是 babel 相关知识,但可以从中提取到许多通用知识,这就是现在还去理解 babel 的原因。
|
||||
|
||||
从某个功能点为切面,走一遍框架的完整流程是一种高效的进阶学习方式,如果你也有看到类似这样的文章,欢迎推荐出来。
|
||||
|
||||
> 讨论地址是:[精读《用 Babel 创造自定义 JS 语法》 · Issue #210 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/210)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
|
||||
|
||||
> 关注 **前端精读微信公众号**
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
|
||||
|
||||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
|
||||
@@ -0,0 +1,372 @@
|
||||
## 1 引言
|
||||
|
||||
Flex 与 Grid 相比就像功能键盘和触摸屏。触摸屏的控制力相比功能键盘来说就像是降维打击,因为功能键盘只能上下左右控制(x、y 轴),而触摸屏打破了布局障碍,直接从(z 轴)触达,这样 **无论 UI 内部布局再复杂,都可以通过 touch 直接定位。**
|
||||
|
||||
Flex 是一维布局方式,我们需要不断嵌套 Div 才能形成复杂结构,而一旦布局产生了变化,原有嵌套结构如果不能 “兼容变化” 到新结构,代码就需要重构。而 Grid 就像触摸屏一样,可以二维布局,即便布局方式做了翻天覆地的调整,也仅需少量修改就能适配。
|
||||
|
||||
这就是这次精读 [用 css grid 重新思考布局](https://www.freecodecamp.org/news/css-grid-changes-how-we-can-think-about-structuring-our-content/) 的原因,理解这个革命性布局技术给布局,甚至代码逻辑组织带来的变化。
|
||||
|
||||
## 2 概述
|
||||
|
||||
作者首先抛出了 Flex 的问题,其实是 `block` `float` `flex` 这三种布局模式的通病:
|
||||
|
||||
- 布局结构由 Div 层级结构描述,导致 Div 层级复杂且遇到结构变更时难以维护。
|
||||
- 定制能力弱。Flex 布局有一些不受控制的智能设定,比如宽度 50% 的子元素会被同级元素挤到 50% 以下,这种智能化在某些场景是需要的,但由于没有提供像 Grid 的 `minmax` 之类的 API,所以定制型不足。
|
||||
|
||||

|
||||
|
||||
举个例子,上图的结构用 Flex 描述可能是这样的:
|
||||
|
||||
```html
|
||||
<div class="card">
|
||||
<div class="profile-sidebar">
|
||||
<img src="https://i.pravatar.cc/125?image=3" alt="" class="profile-img" />
|
||||
<ul class="social-list">
|
||||
<li>
|
||||
<a href="#" class="social-link"
|
||||
><i class="fab fa-dribbble-square"></i
|
||||
></a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" class="social-link"
|
||||
><i class="fab fa-facebook-square"></i
|
||||
></a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" class="social-link"
|
||||
><i class="fab fa-twitter-square"></i
|
||||
></a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="profile-body">
|
||||
<h2 class="profile-name">Ramsey Harper</h2>
|
||||
<p class="profile-position">Graphic Designer</p>
|
||||
<p class="profile-info">
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Facere a tempore,
|
||||
dignissimos odit accusantium repellat quidem, sit molestias dolorum
|
||||
placeat quas debitis ipsum esse rerum?
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
利用 HTML 嵌套结构,我们将图形纵向分成两大块,然后在每块内部继续嵌套划分布局,这是最经典的布局行为了。
|
||||
|
||||

|
||||
|
||||
样式文件里,我们需要对每层布局进行描述,同时支持多分辨率弹性布局,包括顶层 `card` 容器在内的一些样式需要做一定调整:
|
||||
|
||||
```scss
|
||||
.card {
|
||||
width: 80%;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 600px;
|
||||
background: #005e9b;
|
||||
flex-basis: 250px;
|
||||
color: white;
|
||||
padding: 2em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.profile-info {
|
||||
font-weight: 300;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.profile-sidebar {
|
||||
margin-right: 2em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.profile-name {
|
||||
letter-spacing: 1px;
|
||||
font-size: 2rem;
|
||||
margin: 0.75em 0 0;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.profile-name::after {
|
||||
content: "";
|
||||
display: block;
|
||||
width: 2em;
|
||||
height: 1px;
|
||||
background: #5bcbf0;
|
||||
margin: 0.5em auto 0.65em;
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
.profile-position {
|
||||
text-transform: uppercase;
|
||||
font-size: 0.875rem;
|
||||
letter-spacing: 3px;
|
||||
margin: 0 0 2em;
|
||||
line-height: 1;
|
||||
color: #5bcbf0;
|
||||
}
|
||||
|
||||
.profile-img {
|
||||
max-width: 100%;
|
||||
border-radius: 50%;
|
||||
border: 2px solid white;
|
||||
}
|
||||
|
||||
.social-list {
|
||||
list-style: none;
|
||||
justify-content: space-evenly;
|
||||
display: flex;
|
||||
min-width: 125px;
|
||||
max-width: 175px;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.social-link {
|
||||
color: #5bcbf0;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.social-link:hover,
|
||||
.social-link:focus {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.bio {
|
||||
padding: 2em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@media (min-width: 450px) {
|
||||
.bio {
|
||||
text-align: left;
|
||||
max-width: 350px;
|
||||
}
|
||||
}
|
||||
|
||||
.bio-title {
|
||||
color: #0090d1;
|
||||
font-size: 1.25rem;
|
||||
letter-spacing: 1px;
|
||||
text-transform: uppercase;
|
||||
line-height: 1;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.bio-body {
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.profile {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
@media (min-width: 450px) {
|
||||
.card {
|
||||
flex-direction: row;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.profile-name::after {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
让我们看看 Grid 是怎么做的吧!Grid 有许多 API,我们重点看 `grid-template-areas` 这个属性,利用它,我们可以不关心模块的 HTML 结构,直接平铺方式描述:
|
||||
|
||||
```html
|
||||
<div class="card">
|
||||
<img src="https://i.pravatar.cc/125?image=3" alt="" class="profile-img" />
|
||||
<ul class="social-list">
|
||||
<li>
|
||||
<a href="#" class="social-link"><i class="fab fa-dribbble-square"></i></a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" class="social-link"><i class="fab fa-facebook-square"></i></a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" class="social-link"><i class="fab fa-twitter-square"></i></a>
|
||||
</li>
|
||||
</ul>
|
||||
<h2 class="profile-name">Ramsey Harper</h2>
|
||||
<p class="profile-position">Graphic Designer</p>
|
||||
<p class="profile-info">
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Facere a tempore,
|
||||
dignissimos odit accusantium repellat quidem, sit molestias dolorum placeat
|
||||
quas debitis ipsum esse rerum?
|
||||
</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
可以看到,使用 Grid 可以将 UI 结构与 HTML 结构分离,HTML 结构仅描述包含关系,我们只需在样式文件中描述具体 UI 结构。
|
||||
|
||||
样式文件只截取 Grid 相关部分:
|
||||
|
||||
```scss
|
||||
.card {
|
||||
width: 80%;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 600px;
|
||||
background: #005e9b;
|
||||
flex-basis: 250px;
|
||||
color: white;
|
||||
padding: 2em;
|
||||
text-align: left;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 3fr;
|
||||
grid-column-gap: 2em;
|
||||
grid-template-areas:
|
||||
"image name"
|
||||
"image position"
|
||||
"social description";
|
||||
}
|
||||
|
||||
.profile-name {
|
||||
grid-area: name;
|
||||
}
|
||||
.profile-position {
|
||||
grid-area: position;
|
||||
}
|
||||
.profile-info {
|
||||
grid-area: description;
|
||||
}
|
||||
.profile-img {
|
||||
grid-area: image;
|
||||
}
|
||||
.social-list {
|
||||
grid-area: social;
|
||||
}
|
||||
```
|
||||
|
||||
可以看到,`grid-template-areas` 是进一步抽象的语法,将页面结构通过直观的文本描述,无论是理解还是修改都更为轻松。
|
||||
|
||||
这种描述方式适配不同分辨率下也具有优势,只要重组 `grid-template-areas` 即可:
|
||||
|
||||
```scss
|
||||
@media (min-width: 600px) {
|
||||
.card {
|
||||
text-align: left;
|
||||
grid-template-columns: 1fr 3fr;
|
||||
grid-template-areas:
|
||||
"image name"
|
||||
"image position"
|
||||
"social description";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
归根结底,Grid 通过二维结构描述,将子元素布局控制收到了父级,使布局描述更加直观。
|
||||
|
||||
最后作者也提到,Flex 依然有使用场景,即简单的一维结构,或者 `space-between` 等 Flex 独有语法的情况。因此推荐整体、复杂的二维布局采用 Grid,一维的简单布局采用 Flex。
|
||||
|
||||
## 3 精读
|
||||
|
||||
Grid 的布局思路给了我很多启发,HTML 结构与 UI 结构的分离有助于减少 DIV 的层级结构,使代码看上去更清晰。
|
||||
|
||||
也许有人会疑惑,Grid 无非将 HTML 布局部分功能挪到了 CSS,整体复杂度应该不变。其实,从 `grid-template-areas` 这个 API 可以看到,Grid 不仅仅将布局功能抽到 CSS 中,更是将布局描述进行了一层抽象,使代码更易维护。
|
||||
|
||||
### 抽象,再抽象
|
||||
|
||||
为什么 Grid 可以对布局进行抽象?因为 Grid 将二维结构都掌握在手中,得到了更大的布局能力,才能进一步将结构化语法抽象为字符串的描述。
|
||||
|
||||
抽象的好处是不言而喻的,你觉得一堆嵌套的 DIV 与下面的代码,哪个更易读呢?
|
||||
|
||||
```scss
|
||||
.card {
|
||||
grid-template-areas:
|
||||
"image name"
|
||||
"image position"
|
||||
"social description";
|
||||
}
|
||||
```
|
||||
|
||||
这就是抽象的好处,一般来说,代码抽象程度越高就越易读,越易维护。
|
||||
|
||||
再看一个 Chrome Grid 插件,将 Grid 可视化显示出来,并可以以 UI 方式进行调整:
|
||||
|
||||

|
||||
|
||||
UI 是对文本的再抽象,同时可以规避一些不可能存在的语法,比如:
|
||||
|
||||
```scss
|
||||
.card {
|
||||
grid-template-areas:
|
||||
"image name"
|
||||
"image position"
|
||||
"social image";
|
||||
}
|
||||
```
|
||||
|
||||
布局只能以凸多边形方式拓展,不可能分离,也不可能突然插入一个其他模块而变成凹多边形。因此 UI 可以将这个错误规避,并简化为横竖多条线的方式对 UI 进行划分,显然这种描述方式效率更高。
|
||||
|
||||
不得不说,Grid 以及图形化插件的探索,是布局领域的一大进步,是不断抽象的尝试,要解决的问题只有一个:如何提供一种更直观的描述 UI 的方式。
|
||||
|
||||
### 布局对模块化的影响
|
||||
|
||||
Grid 将布局方式提高了一个维度,会直接影响到 JS 模块化方式。
|
||||
|
||||
尤其是以 JSX 组织代码的情况下,一个模块等于 UI + JS,通过嵌套方式的布局会让我们更倾向于站在 UI 视角划分模块。
|
||||
|
||||

|
||||
|
||||
比如对于上图模块,如果用 Flex 方式布局,我们可能会首先创建模块 X 作为左侧容器,子元素是 A 和 B,创建模块 Y 作为右侧容器,子元素是 C 以及新容器 Z,Z 容器的子元素是 D 和 E。
|
||||
|
||||
如果你的第一印象是这么组织代码,不得不承认模块化会受到布局方式的影响。虽然许多时候这样划分是正确的,但当这 5 个模块各自没有关联时,我们创建的容器 X、Y、Z 就失去了复用性,在新的组合场景我们又要重新组合一遍。
|
||||
|
||||
但是在 Grid 语法中,我们不需要 X、Y、Z,只需要用 [css grid generator](https://cssgrid-generator.netlify.com/) 按照上图的方式拖拖拽拽即可自动生成如下布局代码:
|
||||
|
||||
```scss
|
||||
.parent {
|
||||
display: grid;
|
||||
grid-template-columns: 3fr repeat(2, 1fr);
|
||||
grid-template-rows: repeat(5, 1fr);
|
||||
grid-column-gap: 0px;
|
||||
grid-row-gap: 0px;
|
||||
}
|
||||
|
||||
.div1 {
|
||||
grid-area: 1 / 1 / 3 / 2;
|
||||
}
|
||||
.div2 {
|
||||
grid-area: 3 / 1 / 6 / 2;
|
||||
}
|
||||
.div3 {
|
||||
grid-area: 1 / 2 / 2 / 4;
|
||||
}
|
||||
.div4 {
|
||||
grid-area: 2 / 2 / 6 / 3;
|
||||
}
|
||||
.div5 {
|
||||
grid-area: 2 / 3 / 6 / 4;
|
||||
}
|
||||
```
|
||||
|
||||
其实 `grid-template-columns` `grid-template-rows` 组合起来使用比 `grid-template-areas` 更强大,但是纯代码方式描述没有 `grid-template-areas` 直观,可是配合一些可视化系统就非常直观了:
|
||||
|
||||

|
||||
|
||||
将 A ~ E 这 5 个模块布局抽出来后,它们之间的关系就打平了,我们可以完全从逻辑视角审视如何做模块化了。
|
||||
|
||||
## 4 总结
|
||||
|
||||
CSS Grid 本质上是一种二维布局的语法,相比 [Block](https://www.w3schools.com/Css/css_inline-block.asp)、[Flex](https://www.w3schools.com/Css/css3_flexbox.asp) 等一维布局方案,多了一个维度可以同时从行与列角度定义布局,因此派生出 `grid-template-areas` 等语法,整体上更内聚更直观,抽象度也更高了。
|
||||
|
||||
理解了这些也就理解了布局未来的发展方向,**让布局与 Dom 分离** 一直是前端的一个梦想,开发 UI 部分时,只需关心页面由哪些模块组成,去实现这些模块就行了,而不需要关心模块之间应该如何组合。在描述组合时,可以通过可视化或比较抽象的字符串描述布局的结构,并对应到写好的模块上,这样的代码维护性远高于用 DIV 描述结构的方案。
|
||||
|
||||
> 讨论地址是:[精读《用 css grid 重新思考布局》 · Issue #211 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/211)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
|
||||
|
||||
> 关注 **前端精读微信公众号**
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
|
||||
|
||||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
|
||||
@@ -0,0 +1,136 @@
|
||||
## 1 引言
|
||||
|
||||
函数式语言在深度学习领域应用很广泛,因为函数式与深度学习模型的契合度很高,[The Beauty of Functional Languages in Deep Learning — Clojure and Haskell](https://www.welcometothejungle.co/fr/articles/btc-deep-learning-clojure-haskell) 就很好的诠释了这个道理。
|
||||
|
||||
通过这篇文章可以加深我们对深度学习与函数式编程的理解。
|
||||
|
||||
## 2 概述与精读
|
||||
|
||||
深度学习是机器学习中基于人工神经网络模型的一个分支,通过模拟多层神经元的自编码神经网络,将特征逐步抽象化,这需要多维度、大数据量的输入。[TensorFlow](https://www.tensorflow.org/) 和 [PyTorch](https://pytorch.org/) 是比较著名的 Python 深度学习框架,同样 [Keras](https://blog.rstudio.com/2017/09/05/keras-for-r/) 在 R 语言中也很著名。然而在生产环境中,基于 **性能和安全性** 的考虑,一般会使用函数式语言 [Clojure](https://www.clojure.org/) 或 [Haskell](https://www.haskell.org/)。
|
||||
|
||||
在生产环境中,可能要并发出里几百万个参数,因此面临的挑战是:如何高效、安全的执行这些运算。
|
||||
|
||||
**所以为什么函数式编程语言可以胜任深度学习的计算要求呢?** 深度学习的计算模型本质上是数学模型,而数学模型本质上和函数式编程思路是一致的:数据不可变且函数间可以任意组合。这意味着使用函数式编程语言可以更好的表达深度学习的计算过程,因此更容易理解与维护,同时函数式语言内置的 Immutable 数据结构也保障了并发的安全性。
|
||||
|
||||
另外函数式语言的函数之间都是相互隔离的,即便在多线程环境下也不会发生竞争和死锁的情况,函数式编程语言会自动处理这些情况。
|
||||
|
||||
比如说 [Clojure](https://www.clojure.org/),**它甚至可在两个同时修改同一引用的程序并发运行时,自动重试其中之一,而不需要手动加锁**:
|
||||
|
||||
```clojure
|
||||
(import ‘(java.util.concurrent Executors))
|
||||
(defn test-stm [nitems nthreads niters]
|
||||
(let [refs (map ref (repeat nitems 0))
|
||||
pool (Executors/newFixedThreadPool nthreads)
|
||||
tasks (map (fn [t]
|
||||
(fn []
|
||||
(dotimes [n niters]
|
||||
(dosync
|
||||
(doseq [r refs]
|
||||
(alter r + 1 t))))))
|
||||
(range nthreads))]
|
||||
(doseq [future (.invokeAll pool tasks)]
|
||||
(.get future))
|
||||
(.shutdown pool)
|
||||
(map deref refs)))
|
||||
(test-stm 10 10 10000) -> (550000 550000 550000 550000 550000 550000 550000 550000 550000 550000)
|
||||
```
|
||||
|
||||
上面的代码创建了引用(refs),同时创建了多个线程自增这个引用对象,按理说每个线程都修改这个引用会导致竞争状态出现,但从结果来看是正常的,说明 Clojure 引擎在执行时会自动解决这个问题。实际上当两个线程出现竞争而失败时,Clojure 会自动重试其中之一。
|
||||
|
||||
> [原文介绍](https://clojure.org/about/concurrent_programming)
|
||||
|
||||
**Clojure 的另一个优势是并行效率高:**
|
||||
|
||||
```clojure
|
||||
(defn calculate-pixels-2 []
|
||||
(let [n (* *width* *height*)
|
||||
work (partition (/ n 16) (range 0 n))
|
||||
result (pmap (fn [x]
|
||||
(doall (map
|
||||
(fn [p]
|
||||
(let [row (rem p *width*) col (int (/ p *height*))]
|
||||
(get-color (process-pixel (/ row (double *width*)) (/ col (double *height*))))))
|
||||
x)))
|
||||
work)]
|
||||
(doall (apply concat result))))
|
||||
```
|
||||
|
||||
使用 `partition` 结合 `pmap` 可以使并发效率达到最大化,也就是 CPU 几乎都消耗在实际计算上,而不是并行的任务管理与上下文切换。Clojure 凭借 `partition` 对计算进行分区,采取分而治之并对分区计算结果进行合并的思路优化了并发性能。
|
||||
|
||||
> [原文介绍](http://www.fatvat.co.uk/2009/05/jvisualvm-and-clojure.html)
|
||||
|
||||
Clojure 另一个特性是函数链式调用:
|
||||
|
||||
```clojure
|
||||
;; pipe arg to function
|
||||
(-> "x" f1) ; "x1"
|
||||
|
||||
;; pipe. function chaining
|
||||
(-> "x" f1 f2) ; "x12"
|
||||
```
|
||||
|
||||
其中 `(-> "x" f1 f2)` 等价于 `f2(f1("x"))`,这种描述不仅更简洁清晰,也更接近于实际数学模型。
|
||||
|
||||
> [原文介绍](http://xahlee.info/clojure/clojure_function_chaining.html)
|
||||
|
||||
最后,Clojure 还具备计算安全性,计算过程不会修改已有的数据,因此在神经网络的任何一层的原始值都会保留,每层计算都可以独立运行且函数永远幂等。
|
||||
|
||||
[Haskell](https://www.haskell.org/) 也有独特的优势,**它具有类型推断、惰性求值等特性**,被认为更适合用于机器学习。
|
||||
|
||||
类型推断即 Haskell 类型都是静态的,如果试图赋予错误的类型会报错。
|
||||
|
||||
Haskell 的另一个优势是可以非常清晰的描述数学模型。
|
||||
|
||||
想想一般数学模型是怎么描述函数的:
|
||||
|
||||
```text
|
||||
fn =>
|
||||
f1 = 1
|
||||
f2 = 9
|
||||
f3 = 16
|
||||
n > 2, fn = 3fn-3 + 2fn-2 + fn-1
|
||||
```
|
||||
|
||||
一般语言用 `if-else` 描述等价关系,但 Haskell 可以几乎原汁原味的还原函数定义过程:
|
||||
|
||||
```haskell
|
||||
solve :: Int -> Interger
|
||||
solve 1 = 1
|
||||
solve 2 = 9
|
||||
solve 3 = 16
|
||||
solve n = 3 * solve (n - 3) + 2 * solve (n - 2) + solve (n - 1)
|
||||
```
|
||||
|
||||
这使得阅读 Haskell 代码和阅读数学公式一样轻松。
|
||||
|
||||
> [原文](https://blog.jle.im/entry/purely-functional-typed-models-1.html)
|
||||
|
||||
Haskell 另一个优势是惰性求值,即计算会在真正用到时才进行,而不会在计算前提前消费掉,比如:
|
||||
|
||||
```haskell
|
||||
let x = [1..]
|
||||
let y = [2,4 ..]
|
||||
head (tail tail( (zip x y)))
|
||||
```
|
||||
|
||||
可以看到,`x` 与 `y` 分别是 `1,2,3,4,5,6...` 与 `2,4,6,8...` 的无限数组,而 `zip` 函数将其整合为一个新数组 `(1,2),(2,4),(3,6),(4,8)...` 这也是无限数组,如果将 `zip` 函数执行完那么程序就会永远执行下去。但 Haskell 却不会陷入死循环,而是直接输出第一位数字 `1`。这就是惰性计算的特性,无论数组有多长,只有真正用到某项时才对其进行计算,所以哪怕初始数据量或计算量很大,实际消耗的运算资源只取决于这次计算实际用到的部分。
|
||||
|
||||
由于深度学习数据量巨大,惰性求值可以忽略海量数据输入,大大提升计算性能。
|
||||
|
||||
## 3 总结
|
||||
|
||||
本文介绍了为什么深度学习更适合使用函数式语言,以及介绍了 Clojure 与 Haskell 语言的共性:安全性、高性能,以及各自独有的特性,证明了为何这两种语言更适合用在深度学习中。
|
||||
|
||||
在前端领域说到函数式或函数之美,大部分时候想到的是 Class Component 与 Function Component 的关系,这个理解是较为片面的。通过本文我们可以了解到,函数式的思想与数学表达式思想如出一辙,以写数学公式的思维方式写代码,就是一种较好的函数式编程思路。
|
||||
|
||||
函数式应该只有表达式,没有语句,这是因为函数式是为了处理运算而诞生的,因此很适合用在深度学习领域。
|
||||
|
||||
> 讨论地址是:[精读《深度学习 - 函数式之美》 · Issue #212 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/212)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
|
||||
|
||||
> 关注 **前端精读微信公众号**
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
|
||||
|
||||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
|
||||
@@ -0,0 +1,309 @@
|
||||
## 1 引言
|
||||
|
||||
[Nuxt](https://github.com/nuxt/nuxt.js) 是基于 Vue 的前端开发框架,这次我们通过 [Introduction toNuxtJS](https://www.youtube.com/watch?v=NS0io3Z75GI) 视频了解框架特色以及前端开发框架的基本要素。
|
||||
|
||||
> nuxt 与 [next](https://github.com/zeit/next.js) 结构很像,可以结合在一起看
|
||||
|
||||
视频介绍了 NuxtJs 的安装、目录结构、页面路由、导航模版、asyncData、meta、vueX。
|
||||
|
||||
这是一个入门级视频,所以上面所列举的特征都是一个前端开发框架的最核心的基本要素。一个前端开发框架,安装、目录结构、页面路由、导航模版一定是最要下功夫认真设计的。
|
||||
|
||||
asyncData 和 Vuex 都在解决数据问题,meta 则是通过约定语法控制网页 meta 属性,这部分值得与 React 体系做对比,在精读部分再展开。
|
||||
|
||||
Nuxtjs 前端开发框架不仅提供了脚手架的基本功能,还对项目结构、代码做了约定,以减少代码量。从这点可以看出,脚手架永远围绕两个核心目标:**让每一行源码都在描述业务逻辑;让每个项目结构都相同且易读**。
|
||||
|
||||
20 年前,几百行 HTML、Css、Js 代码就能完成一个完整的项目,只需要遵守 W3C 的基本规范就足够了,每一个项目代码都简单清晰,而且由于没有复杂的业务逻辑,导致代码结构也非常简单。但现在前端项目复杂度逐渐升高,一个大型项目源码数量可能达到几十万行、几百万行,这是 W3C 规范没有设想到的,因此出现了各种工程化与模块化方案解决这个复杂度问题,也引发了各个框架间约定的割裂,且设计合理程度各不相同。
|
||||
|
||||
Nuxtjs 等框架要做的就是定义支持现代大型项目的前端研发标准,这个规范具有网络效应,即用的人越多,价值越大。
|
||||
|
||||
接下来我们进入正题,看看 Nuxt 脚手架定义了怎样的开发规范。
|
||||
|
||||
## 2 概述
|
||||
|
||||
### 安装
|
||||
|
||||
使用 `npx create-nuxt-app app-name` 创建新项目。这个命令与 `create-react-app` 一样,区别主要是模版以及配置不同。
|
||||
|
||||
这个命令本质上是拉取一个模版到本地,并安装 `nuxt` 系列脚本作为项目依赖,并自动生成一系列 npmScripts:
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"dev": "nuxt",
|
||||
"build": "nuxt build",
|
||||
"start": "nuxt start",
|
||||
"generate": "nuxt generate",
|
||||
"lint": "eslint --ext .js,.vue --ignore-path .gitignore .",
|
||||
"test": "jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"nuxt": "^2.0.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
之后即可通过 `npm start` 等命令开发项目,对大部分项目来说,npmScripts 启动是最能达成共识的。
|
||||
|
||||
这种安装方式另一个好处是,依赖都被安装在了本地,即开发环境 100% 内置在项目中。Nuxt 没有采用全局 cli 命令方式执行,第一是 npmScripts 更符合大家通用习惯,不需要记住不同脚手架繁琐的名称与不同约定的启动命令,第二是全局脚手架一旦进行不兼容升级,老项目就面临维护难题。
|
||||
|
||||
### 目录结构
|
||||
|
||||
```text
|
||||
├── .nuxt
|
||||
├── layouts
|
||||
├── pages
|
||||
├── store
|
||||
├── assets
|
||||
├── static
|
||||
├── middleware
|
||||
├── plugins
|
||||
├── nuxt.config.js
|
||||
```
|
||||
|
||||
**pages**
|
||||
|
||||
页面文件存放的目录,路径 + 文件名即路由名,关于更多约定路由的信息,在下一节页面路由详细说明。
|
||||
|
||||
**layouts**
|
||||
|
||||
模版文件存放的目录,文件名即模版名,页面可以通过定义模版在选择使用的模版。
|
||||
|
||||
**store**
|
||||
|
||||
全局数据流目录,在 vueX 章节介绍。
|
||||
|
||||
**assets**、**static**
|
||||
|
||||
分别存放不需被编译的资源文件与非 `.vue` 的静态文件,比如 scss 文件。
|
||||
|
||||
由于 `.vue` 文件集成了 html、js、css,因此一般不会再额外定义样式文件在 static 文件夹中。
|
||||
|
||||
当然,这是 Vue 生态的特别之处,在 React 生态中会存在大量 `.scss` 文件混杂在各个目录中,比较影响阅读。
|
||||
|
||||
**middleware**、**plugins**
|
||||
|
||||
中间件与插件,这两个目录是可选的,作为一种定制化拓展能力。
|
||||
|
||||
**.nuxt**
|
||||
|
||||
为实现约定路由等便捷功能,启动项目时需要自动生成一些文件作为真正项目入口,这些文件就存储在 `.nuxt` 目录下,gitingore 且无需手动修改。
|
||||
|
||||
**nuxt.config.js**
|
||||
|
||||
nuxt 使用 js 文件作为配置文件,比 json 配置文件拓展性更好一些,这个文件也是整个项目唯一的配置文件。
|
||||
|
||||
基本上 **pages**、**layouts**、**store**、**assets**、以及唯一的配置文件基本成为现代前端开发框架的标配。
|
||||
|
||||
### 页面路由
|
||||
|
||||
nuxt 支持约定路由:
|
||||
|
||||
```text
|
||||
├── pages
|
||||
│ ├── home.vue
|
||||
│ └── index.vue
|
||||
```
|
||||
|
||||
上述目录结构描述了两个路由:`/` 与 `/home`。
|
||||
|
||||
也支持参数路由,只要以下划线作为前缀命名文件,就定义了一个动态参数路由:
|
||||
|
||||
```text
|
||||
├── pages
|
||||
│ ├── videos
|
||||
│ │ └── _id.vue
|
||||
```
|
||||
|
||||
`/videos/*` 都会指向这个文件,且可以通过 `$route.params.id` 拿到这个 url 参数。
|
||||
|
||||
另一个特性是嵌套路由:
|
||||
|
||||
```text
|
||||
├── pages
|
||||
│ ├── videos
|
||||
│ │ └── index.vue
|
||||
│ └── videos.vue
|
||||
```
|
||||
|
||||
`videos.vue` 与 `videos/index.vue` 都指向 `/videos` 这个路由,如果这两个文件同时存在,那么外层的 videos 就会作为外层拦截所有 `/videos` 文件夹下的路由,可以通过 `nuxt-child` 透出子元素:
|
||||
|
||||
```html
|
||||
# pages/videos.vue
|
||||
<template>
|
||||
<div>
|
||||
videos
|
||||
<nuxt-child />
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
### 导航模版
|
||||
|
||||
页面公共逻辑,比如导航条可以放在模版里,模版的目录在 `layouts` 文件夹下。
|
||||
|
||||
默认 `layouts/default.vue` 对所有页面生效,但也可以创建例如 `layouts/videos.vue` 特殊导航文件,在 `pages/` 页面文件通过如下申明指定使用这个模版:
|
||||
|
||||
```html
|
||||
<script>
|
||||
export default {
|
||||
layout: "videos"
|
||||
};
|
||||
</script>
|
||||
```
|
||||
|
||||
### asyncData
|
||||
|
||||
`asyncData` 是 nuxt 支持的异步取数函数,可以替代 `data`。
|
||||
|
||||
`data` 函数:
|
||||
|
||||
```html
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
```
|
||||
|
||||
对于异步场景,可以用 `asyncData` 替代:
|
||||
|
||||
```html
|
||||
<script>
|
||||
export default {
|
||||
async asyncData() {
|
||||
return await fetch("/");
|
||||
}
|
||||
};
|
||||
</script>
|
||||
```
|
||||
|
||||
### meta
|
||||
|
||||
nuxt 允许在 `.vue` 页面文件自定义 head 标签信息:
|
||||
|
||||
```html
|
||||
<script>
|
||||
export default {
|
||||
headr() {
|
||||
return {
|
||||
title: "",
|
||||
meta: {
|
||||
charset: "utf-8"
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
```
|
||||
|
||||
这是开发框架提供的特性,不过在 React 体系下可以通过 `useTitle` 等自定义 Hooks 解决此问题,将框架功能降维到代码功能,会更容易理解些。
|
||||
|
||||
### vueX
|
||||
|
||||
nuxt 集成了 [vuex](https://github.com/vuejs/vuex),在 `store/` 文件夹下创建数据模型:
|
||||
|
||||
```js
|
||||
export const state = () => ({
|
||||
videos: [],
|
||||
currentVideo: {}
|
||||
})
|
||||
|
||||
export const mutations = {
|
||||
SET_VIDEOS (state, videos) {
|
||||
state.videos = videos
|
||||
}
|
||||
SET_CURRENT_VIDEO (state, video) {
|
||||
state.currentVideo = video
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
接下来就能在 `pages` 文件夹下的页面组件使用了:
|
||||
|
||||
```html
|
||||
<script>
|
||||
import { mapState } from "vuex";
|
||||
|
||||
export default {
|
||||
async fetch({ $axios, params, store }) {
|
||||
const reponse = await $axios.get(`/videos/${params.id}`);
|
||||
const video = response.data.data.arrtibutes;
|
||||
store.commit("SET_CURRENT_VIDEO", video);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
```
|
||||
|
||||
将 `return` 替换为 `store.commit` 即可,更多语法可以参考 [vuex 文档](https://github.com/vuejs/vuex)。
|
||||
|
||||
## 3 精读
|
||||
|
||||
Nuxtjs 框架做了几件事情:
|
||||
|
||||
1. 统一执行命令。
|
||||
2. 统一开发框架。
|
||||
3. 统一目录与代码规范。
|
||||
4. 内置公共 utils 函数。
|
||||
|
||||
### 统一执行命令
|
||||
|
||||
命令行是所有开发者每天都要用上十几次甚至几十次的场景,试想一下团队中项目分别有如下这么多不同的启动命令会怎么样?
|
||||
|
||||
1. npm start.
|
||||
2. monkey dev.
|
||||
3. npm run ng.
|
||||
4. npm run bootstrap & banana start.
|
||||
5. ...
|
||||
|
||||
我永远不知道下一个项目该如何启动,这大大降低了开发效率。更严重的是,有的项目可以通过 `npm run docs` 查看文档,有的项目不能;有的项目 `npm run build` 可以触发编译,有的项目却无需编译,等等,所谓的环境不一致或者说迁移成本,学习成本,都是由最开始负责搭建项目脚手架的同学对架构设计不一致导致的,**然而没有必须用 `monkey dev` 才能运行起来的项目,但项目却可能因为被设计为 `monkey dev` 启动而显得与其他项目格格不入,甚至难以统一维护。**
|
||||
|
||||
Nuxtjs 等前端开发框架统一执行命令就是为了解决这个问题,统一开发者习惯需要很长的时间周期,但这个趋势不可挡。
|
||||
|
||||
### 统一开发框架
|
||||
|
||||
**虽然现在 React、Vue、Angular 框架各有利弊,但如果一个团队的项目同时使用了两个以上的框架,没有人会觉得这是一件好事。**
|
||||
|
||||
诚然每个框架都有自己的特点,在不同维度都一些优势,但三大框架能并存,说明各自都没有绝对的杀手锏来消灭对方。
|
||||
|
||||
对开源来说,多元化是活力的源动力,但对一家公司来说,多元化就是一场灾难,至今没有一个框架敢说自己的优势是 “与其他框架混合使用可以提升整体开发效率”。
|
||||
|
||||
前端开发框架要解决的最重要问题也是这一点,无论如何只能选择一种开发框架,Nuxtjs 选择了 Vue,Nextjs 选择了 React。
|
||||
|
||||
### 统一目录与代码规范
|
||||
|
||||
目录和代码规范不会从根本上影响项目的通用性,因为不同的目录结构可以通过映射来兼容,不同的代码规范不会影响代码执行。所以目录与代码规范真正影响的是一个程序员对项目的 “解码成本”。
|
||||
|
||||
所谓解码成本,就是程序员理解项目逻辑所需要的成本。如果你是一个销售主管,让团队周报统一用一种格式汇总绝对比 “用自己喜欢的方式汇总” 效率高,而对编程也一样,一个完全不同的目录结构和代码规范对程序员来说是巨大的阅读阻碍,甚至可能引发恶心反应。
|
||||
|
||||
所以不同的目录结构和代码规范是没有必要的壁垒,除非你的团队已经对某种规范产生达成了牢固的共识,否则最好和其他团队共享相同的目录结构与代码规范。改变代码规范是一件很难得事情,但只要不同规范的团队间产生了长期合作关系,规范统一就势必会被提上议程,那么为何不能在公司层面早一点达成共识,提前消除这种痛苦呢?
|
||||
|
||||
所以统一目录与代码规范是前端开发框架需要优先确定的,很多时候不要去质疑为什么目录叫 `layouts` 而不叫 `layout`,因为这个规范背后形成的协同网络规模越大,叫什么名字就越不重要。
|
||||
|
||||
### 内置公共 utils 函数
|
||||
|
||||
让业务开发更聚焦,还可以通过抽取通用的逻辑的方式解决,但需要解决两个问题:
|
||||
|
||||
1. 虽然将公共函数抽成 npm 包可以解决代码复用问题,但关键是怎么保证你的代码能被别人复用?
|
||||
2. 如何让业务通用的 utils 代码有效沉淀并从项目中移除?
|
||||
|
||||
脚手架内置公共 utils 函数就为了解决这个问题。上面几个小节解决了通用命令、框架、规范,但实际代码中,`router` `history` `fetch` `store` 等等概念也都是可以统一的,**没有一个项目必须用定制的 `fetch` 函数才能取数,但一开始就定制了 `fetch` 会导致耦合了不可预期的、没有必要的业务逻辑,成为理解与提效的阻碍。**
|
||||
|
||||
所以统一这些能统一的包,是进一步提效的关键。也许有人会觉得断了自己造轮子的路,但就像我们如今都不会重写浏览器内核逻辑一样,稳定的逻辑不仅带来了全行业的提效,还催生了前端岗位带来大量的就业,同样的,统一底层通用函数,其实是断了无意义产出这条路,每个人都有追求更高价值事情的权利,不要把自己困在反复造 `fetch` 函数这个低水平的活里。
|
||||
|
||||
## 4 总结
|
||||
|
||||
如果一个项目没有使用类似 Nuxtjs 开发框架,它面临的不仅仅是技术选型不统一的问题,久而久之这种项目势必成为 **代码孤岛**,当尘封在代码仓库几年后,一系列文档工具链接都失效后,就成为谁也不想碰,不敢碰的高危代码。
|
||||
|
||||
所以我们今天不仅要看到 Nuxtjs 提供的能力对项目开发有多么便捷,更要看到这类框架带来的协同效应有多么巨大,如果它不能成为整个前端的标准,至少要成为你们公司,或者你们团队的标准。
|
||||
|
||||
> 讨论地址是:[精读《Nuxtjs》 · Issue #213 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/213)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
|
||||
|
||||
> 关注 **前端精读微信公众号**
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
|
||||
|
||||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
|
||||
@@ -0,0 +1,702 @@
|
||||
## 1 引言
|
||||
|
||||
[React Conf 2019](https://www.youtube.com/watch?v=RCiccdQObpo) 在今年 10 月份举办,内容质量还是一如既往的高,如果想进一步学习前端或者 React,这个大会一定不能错过。
|
||||
|
||||
希望前端精读成为你学习成长路上的布道者,所以本期精读就介绍 React Conf 2019 - Day1 的相关内容。
|
||||
|
||||
总的来看,React Conf 今年的内容视野更广了,不仅仅有技术内容,还有宣扬公益、拓展到移动端、后端,最后还有对 web 发展的总结与展望。
|
||||
|
||||
前端世界正变得越来越复杂,可以看到大家对未来都充满了希望,永不停歇的探索精神是这场大会的主旋律。
|
||||
|
||||
## 2 概述 & 精读
|
||||
|
||||
本期大会思想、设计上的内容较多,具体实现层内容较少,因为行业领导者需要引领规范,而真正技术价值在于思维模型与算法,理解了解题思路,实现它其实并不难。
|
||||
|
||||
### 开发者体验与用户体验
|
||||
|
||||
- 开发者体验:DX(develop experience)
|
||||
- 用户体验:UX(user experience)
|
||||
|
||||
技术人解决的问题总是围绕 DX 与 UX,而一般来说,优化了 DX 往往会带来 UX 的提升,这是因为一个解决开发者体验的技术创新往往也会带来用户体验的升级,至少也能让开发者有更好的心情、更充足的时间做出好产品。
|
||||
|
||||
如何优化开发者体验呢?
|
||||
|
||||
**易上手**
|
||||
|
||||
React 确实致力于解决这个问题,因为 React 实际上是一个开发者桥梁,无论你开发 web、ios 还是单片机,都可以通过一套统一的语法去实现。React 是一个协议标准(读到 reactReconciler 章节会更有体感),React 像 HTML,但 React 不止能构建 HTML 应用,React 希望构建一切。
|
||||
|
||||
**高效开发**
|
||||
|
||||
React 解决调试、工具问题,让开发者更高效的完成工作,这也是开发者体验重要组成部分。
|
||||
|
||||
**弹性**
|
||||
|
||||
React 编写的程序拥有良好可维护性,包括数据驱动、模块化等等特征都是为了更好服务于不同规模的团队。
|
||||
|
||||
对于 UX 问题,React 也有 Concurrent mode、Suspense 等方案。
|
||||
|
||||
虽然 React 还不完美,但 React 致力于解决 DX 与 UX 的目标和效果都是我们有目共睹的,更好的 DX、UX 一定是前端技术未来发展的大趋势。
|
||||
|
||||
### 样式方案
|
||||
|
||||
Facebook 使用 css-in-js,而今年的 React conf 给出了一种技术方案,将 413 kb 的样式文件体积降低到 74kb!
|
||||
|
||||
一步步了解这个方案,从用法开始:
|
||||
|
||||
```tsx
|
||||
const styles = stylex.create({
|
||||
blue: { color: "blue" },
|
||||
red: { color: "red" }
|
||||
});
|
||||
|
||||
function MyComponent(props) {
|
||||
return <span className={styles("blue", "red")}>I'm red now!</span>;
|
||||
}
|
||||
```
|
||||
|
||||
如上是这个方案的写法,通过 `stylex.create` 创建样式,通过 `styles()` 使用样式。
|
||||
|
||||
**主题方案**
|
||||
|
||||
如果使用 CSS 变量定义主题,那么换肤就可以由最外层 `class` 轻松决定了:
|
||||
|
||||
```scss
|
||||
.old-school-theme {
|
||||
--link-text: blue;
|
||||
}
|
||||
|
||||
.text-link {
|
||||
color: var(--link-text);
|
||||
}
|
||||
```
|
||||
|
||||
字体颜色具体的值由外层 `class` 决定,因此外层的 `class` 就可以控制所有子元素的样式:
|
||||
|
||||
```html
|
||||
<div class="old-school-theme">
|
||||
<a class="text-link" href="...">
|
||||
I'm blue!
|
||||
</a>
|
||||
</div>
|
||||
```
|
||||
|
||||
将其封装成 React 组件,也不需要用 `context` 等 JS 能力,而是包裹一层 `class` 即可。
|
||||
|
||||
```tsx
|
||||
function ThemeProvider({ children, theme }) {
|
||||
return <div className={themes[theme]}>{children}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
**图标方案**
|
||||
|
||||
下面是设计师给出的 svg 代码:
|
||||
|
||||
```tsx
|
||||
<svg viewBox="0 0 100 100">
|
||||
<path d="M9 25C8 25 8..." />
|
||||
</svg>
|
||||
```
|
||||
|
||||
将其包装为 React 组件:
|
||||
|
||||
```tsx
|
||||
function SettingsIcon(props) {
|
||||
return (
|
||||
<SVGIcon viewBox="0 0 100 100" {...props}>
|
||||
<path d="M9 25C8 25 8..." />
|
||||
</SVGIcon>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
结合上面提到的主题方案,就可以控制 svg 的主题颜色。
|
||||
|
||||
```tsx
|
||||
const styles = stylex.create({
|
||||
primary: { fill: "var(--primary-icon)" },
|
||||
gighlight: { fill: "var(--highlight-icon)" }
|
||||
});
|
||||
|
||||
function SVGIcon(color, ...props) {
|
||||
return (
|
||||
<svg>
|
||||
{...props}
|
||||
className={styles({
|
||||
primary: color === "primary",
|
||||
highlight: color === "highlight"
|
||||
})}
|
||||
{children}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**减少样式大小的秘密**
|
||||
|
||||
```tsx
|
||||
const styles = stylex.create({
|
||||
blue: { color: "blue" },
|
||||
default: { color: "red", fontSize: 16 }
|
||||
});
|
||||
|
||||
function MyComponent(props) {
|
||||
return <span className={styles("default", props.isBlue && "blue")} />;
|
||||
}
|
||||
```
|
||||
|
||||
对于上述样式文件代码,最终会编译成 `c1`、`c2`、`c3` 三个 `class`:
|
||||
|
||||
```scss
|
||||
.c1 {
|
||||
color: blue;
|
||||
}
|
||||
.c2 {
|
||||
color: red;
|
||||
}
|
||||
.c3 {
|
||||
font-size: 16px;
|
||||
}
|
||||
```
|
||||
|
||||
出乎意料的是,并没有根据 `blue` 和 `default` 生成对应的 `class`,而是根据实际样式值生成 `class`,这样做有什么好处呢?
|
||||
|
||||
首先是加载顺序,`class` 生效的顺序与加载顺序有关,而按照样式值生成的 `class` 可以精确控制样式加载顺序,使其与书写顺序对应:
|
||||
|
||||
```tsx
|
||||
// 效果可能是 blue 而不是 red
|
||||
<div className="blue red" />
|
||||
|
||||
// 效果一定是 red,因为 css-in-js 在最终编排 class 时,虽然两种样式都存在,但书写顺序导致最后一个优先级最高,
|
||||
// 合并的时候就会舍弃失效的那个 class
|
||||
<div className={styles('blue', 'red')} />
|
||||
```
|
||||
|
||||
这么做永远不会出现头疼的样式覆盖问题。
|
||||
|
||||
更重要的是,随着样式文件的增多,`class` 总量会减少。这是因为新增的 `class` 涵盖的属性可能已经被其他 `class` 写到并生成了,此时会直接复用对应属性生成的 `class` 而不会生成新的:
|
||||
|
||||
```tsx
|
||||
<Component1 className=".class1"/>
|
||||
<Component2 className=".class2"/>
|
||||
```
|
||||
|
||||
```scss
|
||||
.class1 {
|
||||
background-color: mediumseagreen;
|
||||
cursor: default;
|
||||
margin-left: 0px;
|
||||
}
|
||||
.class2 {
|
||||
background-color: thistle;
|
||||
cursor: default;
|
||||
justify-self: flex-start;
|
||||
margin-left: 0px;
|
||||
}
|
||||
```
|
||||
|
||||
正如这个 Demo 所示,正常情况的 `class1` 与 `class2` 存在许多重复定义的属性,但换成 css-in-js 的方案,编译后的效果等价于将 `class` 复用并拆解了:
|
||||
|
||||
```tsx
|
||||
<Component1 classNames=".classA .classB .classD">
|
||||
|
||||
<Component2 classNames=".classA .classC .classD .classE">
|
||||
```
|
||||
|
||||
```scss
|
||||
.classA {
|
||||
cursor: default;
|
||||
}
|
||||
.classB {
|
||||
background-color: mediumseagreen;
|
||||
}
|
||||
.classC {
|
||||
background-color: thistle;
|
||||
}
|
||||
.classD {
|
||||
margin-left: 0px;
|
||||
}
|
||||
.classE {
|
||||
justify-self: flex-start;
|
||||
}
|
||||
```
|
||||
|
||||
这种方式不仅节省空间、还能自动计算样式优先级避免冲突,并将 413 kb 的样式文件体积降低到 74kb。
|
||||
|
||||
### 字体大小方案
|
||||
|
||||
`rem` 的好处是相对的字体大小,使用 `rem` 作为单位可以很方便实现网页字体大小的切换。
|
||||
|
||||
但问题是现在工业设计都习惯了以 px 作为单位,所以一种全新的编译方案产生了:在编译阶段将 `px` 自动转换成 `rem`。
|
||||
|
||||
这等于让以 `px` 为单位的字体大小可以跟随根节点字体大小随意缩放。
|
||||
|
||||
### 代码检测
|
||||
|
||||
静态检测类型错误、拼写错误、浏览器兼容问题。
|
||||
|
||||
在线检测 dom 节点元素问题,比如是否有可访问性,比如替代文案 aria-label。
|
||||
|
||||
### 提升加载速度
|
||||
|
||||
普通网页的加载流程是这样的:
|
||||
|
||||

|
||||
|
||||
先加载代码,然后会渲染页面,在渲染的同时发取数请求,等取数完成后才能渲染出真实数据。
|
||||
|
||||
那么如何改善这个情况呢?首先是预取数,提前解析出请求并在脚本加载的同时取数,可以节省大量时间:
|
||||
|
||||

|
||||
|
||||
那么下载的代码可以再拆分吗?注意到并不是所有代码都作用于 UI 渲染,我们可以将模块分为 `ImportForDisplay` 与 `importForAfterDisplay` :
|
||||
|
||||

|
||||
|
||||
这样就可以优先加载与 UI 相关的代码,其余逻辑代码在页面展示出之后再加载:
|
||||
|
||||

|
||||
|
||||
这样可以实现源码分段加载,并分段渲染:
|
||||
|
||||

|
||||
|
||||
对取数来说也是如此,并不是所有取数都是初始化渲染阶段必须用上的。可以通过 `relay` 的特性 `@defer` 标记出可以延迟加载的数据:
|
||||
|
||||
```relay
|
||||
fragment ProfileData on User {
|
||||
classNameprofile_picture { ... }
|
||||
|
||||
...AdditionalData @defer
|
||||
}
|
||||
```
|
||||
|
||||
这下取数也可以分段了,首屏的数据会优先加载:
|
||||
|
||||

|
||||
|
||||
利用 `relay` 还可以以数据驱动方式结合代码拆分:
|
||||
|
||||
```relay
|
||||
... on Post {
|
||||
... on PhotoPost {
|
||||
@module('PhotoComponent.js')
|
||||
photo_data
|
||||
}
|
||||
|
||||
... on VideoPost {
|
||||
@module('VideoComponent.js')
|
||||
video_data
|
||||
}
|
||||
|
||||
... on SongPost {
|
||||
@module('SongComponent.js')
|
||||
song_data
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
这样首屏数据中也只会按需加载用到的部分,请求时间可以再次缩短:
|
||||
|
||||

|
||||
|
||||
可以看到,与 relay 结合可以进一步优化加载性能。
|
||||
|
||||
### 加载体验
|
||||
|
||||
可以 `React.Suspense` 与 `React.lazy` 动态加载组件。通过 `fallback` 指定元素的占位图可以提升加载体验:
|
||||
|
||||
```tsx
|
||||
<React.Suspense fallback={<MyPlaceholder />}>
|
||||
<Post>
|
||||
<Header />
|
||||
<Body />
|
||||
<Reactions />
|
||||
<Comments />
|
||||
</Post>
|
||||
</React.Suspense>
|
||||
```
|
||||
|
||||
`Suspense` 可以被嵌套,资源会按嵌套顺序加载,保证一个自然的视觉连贯性。
|
||||
|
||||
### 智能文档
|
||||
|
||||
通过解析 Markdown 自动生成文档大家已经很熟悉了,也有很多现成的工具可以用,但这次分享的文档系统有意思之处在于,可以动态修改源码并实时生效。
|
||||
|
||||

|
||||
|
||||
不仅如此,还利用了 Typescript + MonacoEditor 在网页上做语法检测与 API 自动提示,这种文档体验上升了一个档次。
|
||||
|
||||
虽然没有透露技术实现细节,但从热更新的操作来看像是把编译工作放在了浏览器 web worker 中,如果是这种实现方式,原理与 [CodeSandbox 实现原理](https://segmentfault.com/a/1190000019679430) 类似。
|
||||
|
||||
### GraphQL and Stuff
|
||||
|
||||
这一段在安利利用接口自动生成 Typescript 代码提升前后端联调效率的工具,比如 go2dts。
|
||||
|
||||
我们团队也开源了基于 swagger 的 Typescript 接口自动生成工具 [pont](https://github.com/alibaba/pont),欢迎使用。
|
||||
|
||||
### React Reconciler
|
||||
|
||||
这是知识密度最大的一节,介绍了如何使用 React Reconclier。
|
||||
|
||||
React Reconclier 可以创建基于任何平台的 React 渲染器,也可以理解为通过 React Reconclier 可以创建自定义的 ReactDOM。
|
||||
|
||||
比如下面的例子,我们尝试用自定义函数 `ReactDOMMini` 渲染 React 组件:
|
||||
|
||||
```jsx
|
||||
import React from "react";
|
||||
import logo from "./logo.svg";
|
||||
import ReactDOMMini from "./react-dom-mini";
|
||||
import "./App.css";
|
||||
|
||||
function App() {
|
||||
const [showLogo, setShowLogo] = React.useState(true);
|
||||
|
||||
let [color, setColor] = React.useState("red");
|
||||
React.useEffect(() => {
|
||||
let colors = ["red", "green", "blue"];
|
||||
let i = 0;
|
||||
let interval = setInterval(() => {
|
||||
i++;
|
||||
setColor(colors[i % 3]);
|
||||
}, 1000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className="App"
|
||||
onClick={() => {
|
||||
setShowLogo(show => !show);
|
||||
}}
|
||||
>
|
||||
<header className="App-header">
|
||||
{showLogo && <img src={logo} className="App-logo" alt="logo /" />}
|
||||
// 自创语法
|
||||
<p bgColor={color}>
|
||||
Edit <code>src/App.js</code> and save to reload.
|
||||
</p>
|
||||
<a
|
||||
className="App-link"
|
||||
href="https://reactjs.org"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Learn React{" "}
|
||||
</a>
|
||||
</header>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ReactDOMMini.render(<App />, codument.getElementById("root"));
|
||||
```
|
||||
|
||||
`ReactDOMMini` 是利用 `ReactReconciler` 生成的自定义组件渲染函数,下面是完整的代码:
|
||||
|
||||
```typescript
|
||||
import ReactReconciler from "react-reconciler";
|
||||
|
||||
const reconciler = ReactReconciler({
|
||||
createInstance(
|
||||
type,
|
||||
props,
|
||||
rootContainerInstance,
|
||||
hostContext,
|
||||
internalInstanceHandle
|
||||
) {
|
||||
const el = document.createElement(type);
|
||||
|
||||
["alt", "className", "href", "rel", "src", "target"].forEach(key => {
|
||||
if (props[key]) {
|
||||
el[key] = props[key];
|
||||
}
|
||||
});
|
||||
|
||||
// React 事件代理
|
||||
if (props.onClick) {
|
||||
el.addEventListener("click", props.onClick);
|
||||
}
|
||||
|
||||
// 自创 api bgColor
|
||||
if (props.bgColor) {
|
||||
el.style.backgroundColor = props.bgColor;
|
||||
}
|
||||
|
||||
return el;
|
||||
},
|
||||
|
||||
createTextInstance(
|
||||
text,
|
||||
rootContainerInstance,
|
||||
hostContext,
|
||||
internalInstanceHandle
|
||||
) {
|
||||
return document.createTextNode(text);
|
||||
},
|
||||
|
||||
appendChildToContainer(container, child) {
|
||||
container.appendChild(child);
|
||||
},
|
||||
appendChild(parent, child) {
|
||||
parent.appendChild(child);
|
||||
},
|
||||
appendInitialChild(parent, child) {
|
||||
parent.appendChild(child);
|
||||
},
|
||||
|
||||
removeChildFromContainer(container, child) {
|
||||
container.removeChild(child);
|
||||
},
|
||||
removeChild(parent, child) {
|
||||
parent.removeChild(child);
|
||||
},
|
||||
insertInContainerBefore(container, child, before) {
|
||||
container.insertBefore(child, before);
|
||||
},
|
||||
insertBefore(parent, child, before) {
|
||||
parent.insertBefore(child, before);
|
||||
},
|
||||
|
||||
prepareUpdate(
|
||||
instance,
|
||||
type,
|
||||
oldProps,
|
||||
newProps,
|
||||
rootContainerInstance,
|
||||
currentHostContext
|
||||
) {
|
||||
let payload;
|
||||
if (oldProps.bgColor !== newProps.bgColor) {
|
||||
payload = { newBgCOlor: newProps.bgColor };
|
||||
}
|
||||
return payload;
|
||||
},
|
||||
commitUpdate(
|
||||
instance,
|
||||
updatePayload,
|
||||
type,
|
||||
oldProps,
|
||||
newProps,
|
||||
finishedWork
|
||||
) {
|
||||
if (updatePayload.newBgColor) {
|
||||
instance.style.backgroundColor = updatePayload.newBgColor;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const ReactDOMMini = {
|
||||
render(wahtToRender, div) {
|
||||
const container = reconciler.createContainer(div, false, false);
|
||||
reconciler.updateContainer(whatToRender, container, null, null);
|
||||
}
|
||||
};
|
||||
|
||||
export default ReactDOMMini;
|
||||
```
|
||||
|
||||
笔者拆解一下说明:
|
||||
|
||||
React 之所以具备跨平台特性,是因为其渲染函数 `ReactReconciler` **只关心如何组织组件与组件间关系,而不关心具体实现**,所以会暴露出一系列回调函数。
|
||||
|
||||
**创建实例**
|
||||
|
||||
由于 React 组件本质是一个描述,即 `tag` + 属性,所以 `Reconciler` 不关心元素是如何创建的,需要通过 `createInstance` 拿到组件基本属性,在 Web 平台利用 DOM API 实现:
|
||||
|
||||
```typescript
|
||||
createInstance(
|
||||
type,
|
||||
props,
|
||||
rootContainerInstance,
|
||||
hostContext,
|
||||
internalInstanceHandle
|
||||
) {
|
||||
const el = document.createElement(type);
|
||||
|
||||
["alt", "className", "href", "rel", "src", "target"].forEach(key => {
|
||||
if (props[key]) {
|
||||
el[key] = props[key];
|
||||
}
|
||||
});
|
||||
|
||||
// React 事件代理
|
||||
if (props.onClick) {
|
||||
el.addEventListener("click", props.onClick);
|
||||
}
|
||||
|
||||
// 自创 api bgColor
|
||||
if (props.bgColor) {
|
||||
el.style.backgroundColor = props.bgColor;
|
||||
}
|
||||
|
||||
return el;
|
||||
}
|
||||
```
|
||||
|
||||
之所以说 React 对 DOM 事件都做了一层代理,是因为 JSX 的所有函数都没有真正透传给 DOM,而是通过类似 `el.addEventListener("click", props.onClick)` 的方式代理实现的。
|
||||
|
||||
而自定义这个函数,我们甚至能创建例如 `bgColor` 这种特殊语法,只要解析引擎实现了这个语法的 Handler。
|
||||
|
||||
除此之外,还有 **创建、删除实例** 的回调函数,我们都要利用 DOM 平台的 API 重新实现一遍,这样不仅可以实现对浏览器 API 的兼容,还可以对接到比如 react-native 等非 WEB 平台。
|
||||
|
||||
**更新组件**
|
||||
|
||||
实现了 `prepareUpdate` 与 `commitUpdate` 才能完成组件更新。
|
||||
|
||||
`prepareUpdate` 返回的 `payload` 被 `commitUpdate` 函数接收到,并根据接收到的信息决定如何更新实例节点。这个实例节点就是 `createInstance` 回调函数返回的对象,所以如果在 WEB 环境返回的 instance 就是 DOMInstance,后续所有操作都使用 DOMAPI。
|
||||
|
||||
总结一下:`react` 主要用平台无关的语法生成具有业务含义的 AST,而利用 `react-reconciler` 生成的渲染函数可以解析这个 AST,并提供了一系列回调函数实现完整的 UI 渲染功能,`react-dom` 现在也是基于 `react-reconciler` 写的。
|
||||
|
||||
### 图标体积优化
|
||||
|
||||
Facebook 团队通过优化,将图标大小从 4046.05KB 降低到了 132.95kb,体积减少了惊人的 96.7%,减少体积占总包体积的 19.6%!
|
||||
|
||||
实现方式很简单,下面是原始图标使用的代码:
|
||||
|
||||
```jsx
|
||||
<FontAwesomeIcon icon="coffee" />
|
||||
<Icon icon={["fab", "twitter"]} />
|
||||
<Button leftIcon="user" />
|
||||
<FeatureGroup.Item icon="info" />
|
||||
<FeatureGroup.Item icon={["fail", "info"]} />
|
||||
```
|
||||
|
||||
在编译期间通过 AST 分析,将所有字符串引用换成了图标实例的引用,利用 webpack 的 tree-shaking 功能实现按需加载,从而删除了没有使用到的图标。
|
||||
|
||||
```jsx
|
||||
import {faCoffee,faInfo,faUser} from "@fontawesome/free-solid-svg-icons"
|
||||
import {faTwitter} from '@fontawesome/free-brands-svg-icons'
|
||||
import {faInfo as faInfoFal} from '@fontawesome/pro-light-svg-icons'
|
||||
|
||||
<FontAwesomeIcon icon={faCoffee} />
|
||||
<Icon icon={faTwitter} />
|
||||
<Button leftIcon={faUser} />
|
||||
<FeatureGroup.Item icon={faInfo} />
|
||||
<FeatureGroup.Item icon={faInfoFal} />
|
||||
```
|
||||
|
||||
[替换工具](https://github.com/skovy/font-awesome-codemod) 的链接放出来了,感兴趣的同学可以点进去了解更多。
|
||||
|
||||
这也从某种意义上说明了 iconFont 注定被淘汰,因为字体文件目前无法按需加载,只有全部使用 SVG 图标的项目才能使用这种优化。
|
||||
|
||||
### Git & Github
|
||||
|
||||
这一节介绍了基本 Git 知识以及 Github 用法,笔者略过比较水的部分,直接列出两个可能你不知道的点:
|
||||
|
||||
**干预 Github 项目主要语言检测**
|
||||
|
||||
如果你提交的代码包含许多自动生成的文件,可能你实际使用的语言不会被 Github 解析为主要语言,这时候可以通过 `.gitattributes` 文件忽略指定文件夹的检测:
|
||||
|
||||
```text
|
||||
static/* linguist-vendored
|
||||
```
|
||||
|
||||
这样语言文件占比统计就会忽略 `static/` 文件夹。
|
||||
|
||||
**Git hooks 的技巧**
|
||||
|
||||
以下是几个比较具有启发的点,我们可以利用 Git hooks 做点什么:
|
||||
|
||||
- 阻止提交到 master。
|
||||
- 在 commit 之前执行 prettier/eslint/jest 检测。
|
||||
- 检测代码规范、合并冲突、检测是否有大文件。
|
||||
- commit 成功后给出提示或记录到日志。
|
||||
|
||||
但 Git hooks 仍然有局限性:
|
||||
|
||||
- 容易被绕过:--no-verifuy --no-merge --no-checkout ---force。
|
||||
- 本地 hooks 无法提交,导致项目开发规则可能不尽相同。
|
||||
- 无法替代 CI、服务端分支保护、Code Review。
|
||||
|
||||
可以畅想一下,在 WebIDE 环境可以通过自定义 git 命令禁止检测绕过,自然解决第二条环境不一致的问题。
|
||||
|
||||
### GraphQL + Typescript
|
||||
|
||||
GraphQL 是没有类型支持的,如果要手动创建一遍类型文件是非常痛苦的:
|
||||
|
||||
```typescript
|
||||
interface GetArticleData {
|
||||
getArticle: {
|
||||
id: number;
|
||||
title: string;
|
||||
};
|
||||
}
|
||||
|
||||
const query = graphql(gql`
|
||||
query getArticle {
|
||||
article {
|
||||
id
|
||||
title
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
apolloClient.query<GetArticleData>(query);
|
||||
```
|
||||
|
||||
同样的代码分散在两处维护一定会带来问题,我们可以利用比如 `typed-graphqlify` 这种库解决类型问题:
|
||||
|
||||
```typescript
|
||||
import { params, types, query } from "typed-graphqlify";
|
||||
|
||||
const getArticleQuery = {
|
||||
article: params({
|
||||
id: types.number,
|
||||
title: types.string
|
||||
})
|
||||
};
|
||||
|
||||
const gqlString = query("getUser", getUserQuery);
|
||||
```
|
||||
|
||||
只要一遍定义就可以自动生成 GQLString,并且拿到 Typescript 类型。
|
||||
|
||||
### React 文档国际化
|
||||
|
||||
即便是谷歌翻译也不是很靠谱,国际化文档还是要靠人肉,[Nat Alison](https://github.com/tesseralis) 利用 Github 充分发动各国人民的力量,共同打造了一个个 reactjs group 下的国际化仓库。
|
||||
|
||||
国际化仓库命名规则是 `reactjs/xx.reactjs.org`,比如简体中文的国际化仓库是:https://github.com/reactjs/zh-hans.reactjs.org
|
||||
|
||||
从仓库的 readme 可以看到维护规则是这样的:
|
||||
|
||||
- 请 fork 这个仓库。
|
||||
- 基于 fork 后的仓库中 master 分支拉取一个新的分支(名字自取)。
|
||||
- 翻译(校对)你所选择的文章,提交到新的分支。
|
||||
- 此时提交 Pull Request 到该仓库。
|
||||
- 会有专人 Review 该 Pull Request,当两人以上通过该 Pull Request 时,你的翻译将被合并到仓库中。
|
||||
- 删除你所创建的分支(如继续参与,参考同步流程)。
|
||||
|
||||
之后定期从 React 官方文档项目拉取最新代码即可保持文档的同步更新。
|
||||
|
||||
### 你需要 redux 吗?
|
||||
|
||||
关于数据流的话题目前没有什么新意,但这次 React Conf 关于数据流总结的算是比较真诚的,总结了以下几个点:
|
||||
|
||||
1. 全局数据流现在不是必须的,比如 Redux,但也不能说完全不能用,至少在全局状态较为复杂时有必要使用。
|
||||
2. 不要只使用一种数据流方案,根据状态的作用域确定方案比较好。
|
||||
3. 工程技术与科学不同,工程世界没有最好的方案,只有更好的方案。
|
||||
4. 就算有了完美方案也不要停止学习的步伐,总会有新知识产生。
|
||||
|
||||
### web 历史
|
||||
|
||||
很精彩的演讲,不过新鲜内容并不多,比较有感触一点是:以前的网页地址对应到的是服务器磁盘的某个具体文件,比如早期 php 应用,现在后端不再是文件化而是服务化了,这层抽象让服务端摆脱了对文件结构的依赖,可以构建更多复杂动态逻辑,也支持了前后端分离的技术方案。
|
||||
|
||||
## 3 总结
|
||||
|
||||
这届 React Conf 让我们看到前端更多的可能性,我们不仅要关注技术实现细节,更要关注行业标准以及团队愿景。
|
||||
|
||||
React 团队的愿景是让 React 包罗万象,提升全球开发者的开发体验、提升全球产品的用户体验,基于这个目标,React Conf 自然不能只包含 DOM Diff、Reconciler 等等技术细节,更需要展示 React 如何帮助全球开发者,如何让这些开发者帮助到用户,如何推动行业标准的演进,如何让 React 打破国界、语言的壁垒。
|
||||
|
||||
相比其他前端大会非常多的干货来说,React Conf 虽然显得主题比较杂,但这正是人文情怀的体现,我相信只有带着更高的使命愿景,真诚帮助他人的技术团队才可以走得更远。
|
||||
|
||||
> 讨论地址是:[精读《React Conf 2019 - Day1》 · Issue #214 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/214)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
|
||||
|
||||
> 关注 **前端精读微信公众号**
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
|
||||
|
||||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
|
||||
@@ -0,0 +1,630 @@
|
||||
## 1 引言
|
||||
|
||||
这是继 [精读《React Conf 2019 - Day1》](https://github.com/dt-fe/weekly/blob/v2/127.%E7%B2%BE%E8%AF%BB%E3%80%8AReact%20Conf%202019%20-%20Day1%E3%80%8B.md) 之后的第二篇,补充了 React Conf 2019 第二天的内容。
|
||||
|
||||
## 2 概述 & 精读
|
||||
|
||||
第二天的内容更为精彩,笔者会重点介绍比较干货的部分。
|
||||
|
||||
### Fast refresh
|
||||
|
||||
[Fast refresh](https://facebook.github.io/react-native/docs/fast-refresh) 是更好的 react-hot-loader 替代方案,目前仅支持 react-native 平台,很快就会支持 react-dom 平台。
|
||||
|
||||
相比不支持 Function component、无法错误恢复、更新经常失灵的 hot reloading 来说,fast refresh 还拥有以下几个优点:
|
||||
|
||||
- 状态保持。
|
||||
- 支持 Function Component Hooks。
|
||||
- 更快的更新速度。
|
||||
|
||||
Fast refresh 更新速度更快,是基于 Function Component 生成了 “签名”,从而最大成都避免销毁重渲染,尽可能保持对组件的 rerender 刷新。下面介绍签名机制的工作原理。
|
||||
|
||||
Fast refresh 对每个 Function component 都生成了一份专属签名,用以描述这个组件核心状态,当这个核心状态改变时,就只能销毁重渲染了,但对于不触及核心的修改就能进行代价非常小的 rerender。
|
||||
|
||||
这个签名包含了 hooks 和参数名:
|
||||
|
||||
```js
|
||||
// signature: "useState{isLoggedIn}"
|
||||
|
||||
function ExampleComponent() {
|
||||
const [isLoggedIn, setIsLoggedIn] = useState(true);
|
||||
}
|
||||
```
|
||||
|
||||
比如当参数名变更时,这个组件的逻辑已发生改动,此时只能销毁并重渲染了。因此实际上通过对签名的对比来判断是否要销毁并重刷新组件:
|
||||
|
||||
```js
|
||||
// signature: "useState{isLoggedOut}"
|
||||
|
||||
function ExampleComponent() {
|
||||
const [isLoggedOut, setIsLoggedOut] = useState(true);
|
||||
}
|
||||
```
|
||||
|
||||
同理,当 hooks 从 `useState` 改成了 `useReducer`,签名也会发生变化从而导致彻底的重渲染。
|
||||
|
||||
但除此之外,**比如对样式的修改、Dom 结构的修改都不会触发签名的变化**,从而保证了 “对不触及逻辑的改动进行高效的轻量 renreder”。
|
||||
|
||||
然而 Fast refresh 也有如下局限性:
|
||||
|
||||
- 还不能友好支持 Class component。
|
||||
- 混合导出 React 和非 React 组件时无法精确的 hot reload。
|
||||
- 更高的内存要求。
|
||||
|
||||
可以看到,Fast Refresh 随着功能推广与内置,现在已经覆盖了 Facebook 95% 以上 hot reload 场景了:
|
||||
|
||||
<img width=500 src="https://img.alicdn.com/tfs/TB1bjm1mYr1gK0jSZR0XXbP8XXa-1598-1018.png">
|
||||
|
||||
这部分内容不仅揭开了 hot reload 技术内幕,还对其功能进行了进一步优化,2019 年的 React 开发体系已经进入精细化阶段。
|
||||
|
||||
### 重写 React devtools
|
||||
|
||||
React devtools 的更新终于被正式介绍了,本来笔者以为新的 devtools 只是支持了 hooks,但听完分享后发现还有更多有用的改进,包括:
|
||||
|
||||
- 更高的性能。
|
||||
- 更多特性支持。
|
||||
- 更好用户体验。
|
||||
|
||||
**找到节点渲染链路**
|
||||
|
||||
并不是每个 React 节点都参与渲染,新版 React devtools 可以展示出 rendered by:
|
||||
|
||||
<img width=500 src="https://img.alicdn.com/tfs/TB1OSWomV67gK0jSZPfXXahhFXa-2354-668.png">
|
||||
|
||||
**调试 Suspense**
|
||||
|
||||
在 Day1 中讲到的 Suspense 特性可以在 React devtools 调试了:
|
||||
|
||||
<img width=500 src="https://img.alicdn.com/tfs/TB1W790m4D1gK0jSZFsXXbldVXa-1816-660.png">
|
||||
|
||||
通过点击时钟 icon,可以模拟 Suspense 处于 pendding 或 ready 状态。
|
||||
|
||||
**增强调试能力**
|
||||
|
||||
可以通过点击直接跳转到组件源码:
|
||||
|
||||
<img width=500 src="https://img.alicdn.com/tfs/TB1IBK1m4n1gK0jSZKPXXXvUXXa-1806-772.png">
|
||||
|
||||
最新版已增强至点击按钮后直接通过 Source 打开源码位置,**这样可以快速通过 UI 寻找到代码**。同时还可以看到,通过点击 debugger 按钮将当前组件信息打到控制台调试。
|
||||
|
||||
除此之外还可以动态修改组件的 props 与 hook state,大大增强了调试能力。
|
||||
|
||||
**profiler**
|
||||
|
||||
分析工具也得到了增强,现在可以看到每个组件被渲染了几次以及重新渲染的原因:
|
||||
|
||||
<img width=500 src="https://img.alicdn.com/tfs/TB1cla1m7P2gK0jSZPxXXacQpXa-1824-690.png">
|
||||
|
||||
比如上图组件被渲染了 4 次,主要有两个原因:Hooks 改变与 Props 改变。
|
||||
|
||||
除此之外,还优化了更多细节体验,比如高亮搜索、HOC 的展示优化、嵌套层级过多时不会占用过多的横向宽度等等。
|
||||
|
||||
### react codemod
|
||||
|
||||
codemod 是一个代码重构的方式,通过 AST 方式精准触达代码,我们可以认为 codemod 是一个更聪明的“查找/替换”。
|
||||
|
||||
codemod 主要有以下三种使用方式:
|
||||
|
||||
- 重命名。
|
||||
- 代码排序。
|
||||
- 一定程度的代码替换。
|
||||
|
||||
接下来就讲到 [react codemod](https://github.com/reactjs/react-codemod) 了,它是 react 场景的 codemod 解决方案,facebook 是这么使用 react codemod 的:
|
||||
|
||||
- 迁移 facebook 代码。
|
||||
- 涉及几万个组件。
|
||||
- 修复了 3500 个文件的 React.PropTypes。
|
||||
- 修复了 8500 个文件的生命周期 unsafe。
|
||||
- 修复了 20000 个文件的 createClass 转 JSX。
|
||||
|
||||
使用方式:
|
||||
|
||||
```bash
|
||||
npx react-codemod React-PropTypes-to-prop-types
|
||||
```
|
||||
|
||||
可以看到,通过 cli 对文件进行一次性重构处理。除此之外,再列举几种使用场景:
|
||||
|
||||
- create-element-to-jsx 将 `React.createElement` 转换为 JSX。
|
||||
- error-boundaries 将 `unstable_handleError` 改为 `componentDidCatch`。
|
||||
- findDOMNode 将 `React.createClass` 中 `this.getDOMNode()` 改为 `React.findDOMNode`。
|
||||
- sort-comp 将 Class Component 生命周期按照规范排序,[eslint-plugin-react](https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/sort-comp.md) 插件也有相同能力。
|
||||
|
||||
理论上来讲,所有 codemode 做的事情都可以替换为 eslint 的 autofix 来完成,比如 sort-comp 就同时被 codemode 和 eslint 支持。
|
||||
|
||||
### Suspense
|
||||
|
||||
要理解 Suspense,就要理解 Suspense 与普通 loading 有什么区别。
|
||||
|
||||
从代码角度来说,Suspense 可以类比为 `try/catch` 的体验。为了简化代码复杂度,我们可以用 `try/catch` 包裹代码,从而简化 try 区块代码复杂度,并将兜底代码放在 catch 区块:
|
||||
|
||||
```js
|
||||
try {
|
||||
// 只要考虑正确情况
|
||||
} catch {
|
||||
// 错误时 fallback
|
||||
}
|
||||
```
|
||||
|
||||
Suspense 也一样,它在渲染 React 组件时如果遇到了 Promise 抛出的 Error,就会进入 `fallback`,所以 `fallback` 含义是 Loading 中状态:
|
||||
|
||||
```jsx
|
||||
<Suspense fallback={<Spinner />}>
|
||||
<ProfilePage />
|
||||
</Suspense>
|
||||
```
|
||||
|
||||
与此同时,实际业务组件中的取数也不需要担心取数是否正在进行中,只要直接处理拿到数据的情况就好了:
|
||||
|
||||
```jsx
|
||||
function ProfileDetails() {
|
||||
// 直接使用 user,不用担心失败。
|
||||
const user = resource.user.read();
|
||||
return <h1>{user.name}</h1>;
|
||||
}
|
||||
```
|
||||
|
||||
进一步的,如果要处理组件渲染的异常,再使用 `ErrorBoundary` 包裹即可,此时的 `fallback` 含义是组件加载异常的错误状态:
|
||||
|
||||
```jsx
|
||||
function Home(props) {
|
||||
return (
|
||||
<ErrorBoundary fallback={<ErrorMessage />}>
|
||||
<Suspense fallback={<Placeholder />}>
|
||||
<Composer />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Suspense 模式的取数好处是 “fetch on render”,即渲染与取数同时进行,而普通模式的取数是 “fetch after render”,即渲染完成后再通过 `useEffect` 取数,此时取数时机已晚。
|
||||
|
||||
**队列加载**
|
||||
|
||||
假设 `Composer` 与 `NewsFeed` 组件内部都通过 `useQuery` 取数,那么并行取数时加载机制如下:
|
||||
|
||||
<img width=500 src="https://img.alicdn.com/tfs/TB1AonZm7L0gK0jSZFtXXXQCXXa-1770-778.png">
|
||||
|
||||
这可能有两个问题:组件内部加载顺序不统一与组件间加载顺序不统一。
|
||||
|
||||
如果组件内部有图片,可能图片与组件渲染实际不一致,此时可以利用 Suspense 统一 hold 所有子组件的特性,将图片加载改为 Suspense 模式:
|
||||
|
||||
```jsx
|
||||
<div>
|
||||
<YourImage src={uri} alt={...} />
|
||||
<MoreComposer />
|
||||
</div>
|
||||
```
|
||||
|
||||
同一个 Suspense 可以等待所有子元素都 Ready 后才会一把渲染出 UI,因此可以看到网页被一次性刷新而不是分部刷新。
|
||||
|
||||
第二个问题是组件间加载顺序不统一,可能导致先渲染了文章内容,再渲染出文章头部,此时如果区块高度不固定,文章头部可能会撑开,导致文章内容下移,用户的阅读体验会遭到打断。可以通过 `suspense ordering` 解决这个问题:
|
||||
|
||||
```jsx
|
||||
function Home(props) {
|
||||
return (
|
||||
<SuspenseList revealOrder="forwards">
|
||||
<Suspense fallback={<ComposerFallback />}>
|
||||
<Composer />
|
||||
</Suspense>
|
||||
<Suspense fallback={<FeedFallback />}>
|
||||
<NewsFeed />
|
||||
</Suspense>
|
||||
</SuspenseList>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
比如 `forwards` 表示从上到下,那么一定会先渲染头部再渲染文章内容,这样文章内容就不会都抖动了。
|
||||
|
||||
### Render as you fetch
|
||||
|
||||
相比 “fetch on render”,更高级别的优化是 “Render as you fetch”,即取数在渲染时机之前。
|
||||
|
||||
比如页面路由的跳转、Hover 到一个区块,此时如果取数由这个动作触发,就可以再次将取数时机提前,Facebook 为此创造了一个新的 Hook:`usePreloadedQuery`。
|
||||
|
||||
用法是,在某个事件中取数,比如点击页面跳转按钮时,通过 `preloadQuery` 预取数,得到的结果并不是取数结果,而是一个标识,在渲染组件中,把这个标识传给 `usePreloadedQuery` 可以拿到真实取数结果:
|
||||
|
||||
```js
|
||||
// 组件 A 的 onClick
|
||||
const reference = preloadQuery(query, variables);
|
||||
// 组件 B 的 render
|
||||
const data = usePreloadedQuery(query, reference);
|
||||
```
|
||||
|
||||
可以看到,取数真正触发的时机在渲染函数执行之前,所以在 `usePreloadedQuery` 调用时取数肯定已经在路上,甚至已经完成。相比之下,普通的 `useQuery` 函数存在下面几个问题:
|
||||
|
||||
- 由于取数过程存在状态变化,可能导致组件在 “取数无意义” 状态下重新渲染多次。
|
||||
- 可能取数还未完成就触发重渲染。
|
||||
- 没有取消的机制,没有清除结果的机制。
|
||||
- 没有办法唯一标识组件。
|
||||
|
||||
preloadQuery 的好处就是将取数时机与 UI 分离,这样可以更细粒度的控制逻辑:
|
||||
|
||||
- 调用 preloadQuery 时:
|
||||
- 在组件销毁时取消取数。
|
||||
- 有新取数触发时取消取数。
|
||||
- 销毁一些轮询机制。
|
||||
- 渲染组件调用 usePreloadedQuery 时:
|
||||
- 不会再触发取数,不会触发意外的 re-render。
|
||||
- 不需要清空,因为取数不在这里发起。
|
||||
- 不需要清理轮询。
|
||||
|
||||
可见 preloadQuery 相比 useQuery 的确有了一些体验提升,然而这个优化比较追求极致,对大部分国内项目来说可能还走不到 facebook 这么极致的性能优化,所以投入产出比显得不是那么高,而且这个开发方式对开发者不是太友好,因为它让请求的时机割裂到两个模块中。
|
||||
|
||||
但毕竟用户体验是大于开发者体验的,React 尽量通过提高开发者体验来间接提高用户体验,使双方都满意,但像 preloadQuery 就无法两者兼顾了,为了用户体验可以适当的降低一些开发者体验。
|
||||
|
||||
### 如何维护代码
|
||||
|
||||
这个分享讲述了如何提升代码维护效率,毕竟一个月后可能连自己写的代码都看不懂了。[hydrosquall](http://github.com/hydrosquall) 通过类比地图的方式解释了程序员是如何维护代码的。
|
||||
|
||||
首先看我们是如何认路的。认路分为三个层次:
|
||||
|
||||
- 随意走走。
|
||||
- 通过一些地标判断方向。
|
||||
- 有方向的寻路。
|
||||
- 通过跟随同伴或者了解更多本地信息找到目的地。
|
||||
- 地图。
|
||||
- 通过 GPS 定位。
|
||||
- 通过模拟地图方式指出路线。
|
||||
|
||||
可以看到这三种方式是逐层递进的,那么类比到代码就有意思了:
|
||||
|
||||
- 随意走走(滚动查看源代码 + ctrl/f 查找代码 + grep 搜索)。
|
||||
- 入口(找到入口节点,查看数据结构)。
|
||||
- 标记(查看代码注释、查看 README)。
|
||||
- 发信号弹(断点、console.log 等调试行为)
|
||||
- 找到方向。
|
||||
- git blame 查看 owner,或直接根据文档找到 codeowners。
|
||||
- 地图。
|
||||
- 幸运的话你可以找到一份架构流程图。
|
||||
|
||||
可以看到,地图有几种抽象层次,比如忽略了细节的纽约地铁线路图:
|
||||
|
||||
<img width=400 src="https://img.alicdn.com/tfs/TB14k7pmYr1gK0jSZR0XXbP8XXa-1014-702.png">
|
||||
|
||||
或者是包含丰富地面信息的地铁线路图:
|
||||
|
||||
<img width=400 src="https://img.alicdn.com/tfs/TB1Sbwpm.T1gK0jSZFrXXcNCXXa-692-750.png">
|
||||
|
||||
抽象到什么层次取决于用户使用的场景,那么代码抽象也是如此。[hydrosquall](http://github.com/hydrosquall) 做了一个工具自动分析出代码调用关系:[js-callgraph](https://github.com/persper/js-callgraph)
|
||||
|
||||

|
||||
|
||||
这就像路牌一样,可以更高效的看出代码结构,也包括了数据流结构,由于篇幅限制,感兴趣的同学可以看 [原视频](https://youtu.be/JDDxR1a15Yo?t=6579) 了解更多。
|
||||
|
||||
### 写作与写代码
|
||||
|
||||
本章讲了写作(小说)与写代码的关联,总结出如下几个重点:
|
||||
|
||||
- 写小说和写代码都是创造行为。
|
||||
- 写代码需要抽象思维,写小说也要有抽象思维构造人物和情节。
|
||||
- Show, don't tell,写作天然就是申明式的,和数据驱动很相似。
|
||||
|
||||
更多可以去看 [原视频](https://youtu.be/JDDxR1a15Yo?t=9135)。
|
||||
|
||||
### 移动端动画最佳实践
|
||||
|
||||
首先要使用一个真实的手机设备调试,否则可能出现 PC Chrome 一切正常,而手机上实际效果性能很差的情况!
|
||||
|
||||
**手势下拉退出**
|
||||
|
||||
利用 [react-spring](https://github.com/react-spring/react-spring) 和 [react-use-gesture](react-use-gesture) 做一个下滑消失的 Demo:
|
||||
|
||||
```jsx
|
||||
import { animated, useSpring } from "react-spring";
|
||||
import { useDrag } from "react-use-gesture";
|
||||
|
||||
const [{ y }, set] = useSpring(() => {
|
||||
y: 0;
|
||||
});
|
||||
```
|
||||
|
||||
首先定义一个 `y` 纵向位置,通过 `useDrag` 将拖拽操作与 UI 绑定,通过回调将其与 `y` 数据绑定:
|
||||
|
||||
```js
|
||||
const bind = useDrag(({ last, movement: [, movementY], memo = y.value }) => {
|
||||
if (last) {
|
||||
// 拖拽结束时,如果偏移量超过 50 则效果和结束一样,直接将 y 设置为 100
|
||||
const notificationClosed = movementY > 50;
|
||||
|
||||
return set({
|
||||
y: notificationClosed ? 100 : 0,
|
||||
onReset: notificationClosed && removeNotification
|
||||
});
|
||||
}
|
||||
|
||||
// y 的位置区间在 0~100
|
||||
set([{ y: clamp(0, 100, memo + movementY) }]);
|
||||
|
||||
return memo;
|
||||
});
|
||||
```
|
||||
|
||||
将 `useDrag` 与 `y` 绑定后,就可以用在 UI 组件上了:
|
||||
|
||||
```jsx
|
||||
<StyledNotification
|
||||
as={animated.div}
|
||||
onTouchStart={bind().onTouchStart}
|
||||
style={{
|
||||
opacity: y.interpolate([0, 100], [1, 0]),
|
||||
transform: y.interpolate(y => `translateY(${y}px)`)
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
将 `opacity` 与 `transform` 与位置 `y` 绑定就可以做出下拉消失的效果。
|
||||
|
||||
**滑动的洞见**
|
||||
|
||||
接着讲到了滑动的三个洞见:
|
||||
|
||||
1. 要立刻响应,任何延迟都会造成用户额外精神负担。
|
||||
2. 滚动速度衰减可以提升用户体验:
|
||||
|
||||
<img width=500 src="https://img.alicdn.com/tfs/TB1HocDm1H2gK0jSZJnXXaT1FXa-1348-878.gif">
|
||||
|
||||
接着我们需要预测用户的意图,比如在一个类似微信消息列表页左右滑动时:
|
||||
|
||||
- 是否想取消手势交互?
|
||||
- 是否想展示出更多交互按钮?
|
||||
- 是否想删除所有内容?
|
||||
|
||||
这需要更多设计思考。
|
||||
|
||||
1. 橡皮筋滚动,即列表页可以一直向下拉,上面部分像橡皮筋一样可以被拉出空白页的效果。
|
||||
|
||||
在设计手势动画时要考虑三个要点:
|
||||
|
||||
- 使用移动增量作为手势动画的基准点。
|
||||
- 动画和手势应该随时可以被中断,通过 springs 即可实现。
|
||||
- 完成手势后的动画速度应该与手势速度相当,这样视觉体验更自然。
|
||||
|
||||
最后提到了动画兼容性与性能,比如尽量只使用 `transform` 与 `opacity` 可以保证移动端的流畅度,不同移动设备的默认手势效果不同,最好通过 `touch-action` 禁用默认行为以达到更好的兼容性与效果。
|
||||
|
||||
### 唱片与 React
|
||||
|
||||
J.Dash 拥有十年软件开发经验,同时也卖过很多唱片,他介绍了唱片行业与软件开发的共同点。
|
||||
|
||||
唱片行业需要音乐编排能力,这与编码能力类似,都存在良好的设计模式,并且需要团队合作,开发过程中会遇到一些痛苦的经历,但最终完成音乐和项目时都会获得满足的喜悦。
|
||||
|
||||
### 函数式编程
|
||||
|
||||
> Declaratives UIs are the future, and the future is Comonadic. - Phil Freeman
|
||||
|
||||
申明式 UI 是未来,未来则是 Comonadic。
|
||||
|
||||
所谓申明式 UI 可以用下面的公式表达:
|
||||
|
||||
```js
|
||||
type render = (state: State) => View;
|
||||
```
|
||||
|
||||
然后用一段公式介绍了 Comonadic:
|
||||
|
||||
```js
|
||||
class Functor w => Comonad w where
|
||||
extract :: w a -> a
|
||||
duplicate :: w a -> w (w a)
|
||||
extend :: (w a -> a) -> w a -> w b
|
||||
```
|
||||
|
||||
用 JS 版本做一个解释:
|
||||
|
||||
```js
|
||||
const Store = ({ state, render }) => ({
|
||||
extend: f => Store({ state, render: state => f(Store({ state, render })) }),
|
||||
extract: () => render(state)
|
||||
});
|
||||
```
|
||||
|
||||
`extract` 调用后会进行申明式渲染 UI,即 `render(state)`。
|
||||
|
||||
`extend` 表示拓展,接收一个拓展函数作为参数,返回一个新的 Store 对象。这个拓展函数可以拿到 `state`、`render` 并返回新的 `state` 作为 `extract` 时 `render` 的输入。使用例子是这样的:
|
||||
|
||||
```jsx
|
||||
const App = Store({
|
||||
state: { msg: "World" },
|
||||
render: ({ msg }) => <p>Hello {msg}</p>
|
||||
});
|
||||
|
||||
App.extend(({ state }) =>
|
||||
state.msg === "World" ? { msg: "ReactConf" } : state
|
||||
).extract(); // <p> Hello ReactConf </p>
|
||||
```
|
||||
|
||||
然而尴尬的是,笔者看了很久也没看懂 `Store` 函数,最后运行了一下发现这个 Demo 抛出了异常 😂。
|
||||
|
||||
下面是笔者稍微修改后的例子,至少能跑起来:
|
||||
|
||||
```js
|
||||
const Store = ({ state, render }) => ({
|
||||
extend: f => Store({ state, render: state => render(f({ state, render })) }),
|
||||
extract: () => render(state)
|
||||
});
|
||||
|
||||
const app = Store({
|
||||
state: { msg: "Hello World" },
|
||||
render: ({ msg }) => console.log("render " + msg)
|
||||
});
|
||||
|
||||
app
|
||||
.extend(({ state }) => {
|
||||
return { msg: state.msg + " extend1" };
|
||||
})
|
||||
.extend(({ state }) => {
|
||||
return { msg: state.msg + " extend2" };
|
||||
})
|
||||
.extract(); // render Hello World extend2 extend1
|
||||
```
|
||||
|
||||
然而作者的意思仍是未解之谜,希望对函数式了解的同学可以在评论区指点一下。
|
||||
|
||||
### wick editor
|
||||
|
||||
[wick editor](https://www.wickeditor.com/) 是一个开源的动画、游戏制作软件。
|
||||
|
||||
wick editor 是一个动画制作工具,但拓展了一些 js 编程能力,因此可以很好的将动画与游戏结合在一起:
|
||||
|
||||
<img width=300 src="https://img.alicdn.com/tfs/TB1hLJpnbr1gK0jSZR0XXbP8XXa-1766-1002.png">
|
||||
|
||||
演讲介绍了 wick editor 的演化过程:
|
||||
|
||||
从很简陋的 MVP 版本开始(1 周)
|
||||
|
||||
<img width=300 src="https://img.alicdn.com/tfs/TB11sdsneL2gK0jSZFmXXc7iXXa-1192-764.png">
|
||||
|
||||
到 Pre-Alpha(4 月)
|
||||
|
||||
<img width=300 src="https://img.alicdn.com/tfs/TB1TMJnnXY7gK0jSZKzXXaikpXa-1306-858.png">
|
||||
|
||||
Alpha(5 月)
|
||||
|
||||
<img width=300 src="https://img.alicdn.com/tfs/TB1608pnoD1gK0jSZFGXXbd3FXa-1794-1186.png">
|
||||
|
||||
Beta(1.5 年)
|
||||
|
||||
<img width=300 src="https://img.alicdn.com/tfs/TB1CKJpnoD1gK0jSZFGXXbd3FXa-1274-854.png">
|
||||
|
||||
重点是 1.0 版本采用 React 重写了!继 Beta 之后又经历了 1 年:
|
||||
|
||||
<img width=300 src="https://img.alicdn.com/tfs/TB1ZZhrneH2gK0jSZFEXXcqMpXa-906-596.png">
|
||||
|
||||
这个团队最棒的地方是,将游戏与教育结合,针对不同场景做了很多用户调研并根据反馈持续改进。
|
||||
|
||||
### React Select
|
||||
|
||||
[react-select](https://github.com/JedWatson/react-select) 的作者 [Jed Watson](https://github.com/JedWatson) 被请来啦。作为一个看上去很简单组件(select)的开发者,却拥有如此大的关注量(1.8w star),那作者有着怎样的心路历程呢?
|
||||
|
||||
react-select 看似简单的名字背后其实有挺多的功能,比如作者列举了一些功能层面的内容:
|
||||
|
||||
- autocomplete - 输入时搜索。
|
||||
- 单、多选。
|
||||
- focus 管理。
|
||||
- 下拉框层级与位置,比如可以放在根 DOM 节点,也可以作为当前节点的子元素。
|
||||
- 异步下拉框内容。
|
||||
- 键盘、触控。
|
||||
- Createble,即在搜索时如果没有内容可以动态创建。
|
||||
- 等等。
|
||||
|
||||
<img width=300 src="https://img.alicdn.com/tfs/TB1RYS1mubviK0jSZFNXXaApXXa-1166-226.png">
|
||||
|
||||
在设计层面:
|
||||
|
||||
- 申明式。
|
||||
- 可以被定制。
|
||||
- 性能要求。
|
||||
- 等等。
|
||||
|
||||
随着 Star 逐渐上涨,越来越多的需求被提出,核心库代码量越来越大,甚至许多需求之间都是相互冲突的,而且作者每天都会被上百个 Issue 与 PR 吵醒。做一个业务 Select 可能只要 5 分钟,但做一个开源 Select 却要 5 年,原因是一个简单的 Select 如何满足所有不同业务场景?这绝对是个巨大的挑战。
|
||||
|
||||
比如用户即需要受控也要非受控的组件,如何满足好这个需求同时又让代码更可维护呢?
|
||||
|
||||
假设我们拥有一个受控的组件 `SelectComponent`,那么它的主要 props 是 `value` 与 `onChange`,如果要拓展成一个既支持 `defaultValue`(非受控)又支持 `value`(受控)的组件,我们可以创建一个 `manageState` 组件对 `SelectComponent` 进行封装:
|
||||
|
||||
```jsx
|
||||
const manageState = SelectComponent => ({
|
||||
value: valueProps,
|
||||
onChange: onChangeProp,
|
||||
defaultValue,
|
||||
...props
|
||||
}) => {
|
||||
const [valueState, setValue] = useState(defaultValue);
|
||||
|
||||
const value = valueProps !== undefined ? valueProps : valueState;
|
||||
|
||||
const onChange = (newValue, actionMeta) => {
|
||||
if (typeof onChangeProp === "function") {
|
||||
onChangeProp(newValue, actionMeta);
|
||||
}
|
||||
setValue(newValue);
|
||||
};
|
||||
|
||||
return <SelectComponent {...props} value={value} onChange={onChange}>
|
||||
};
|
||||
```
|
||||
|
||||
这样就可以组合为一个受控/非受控的综合 Select 组件:
|
||||
|
||||
```js
|
||||
import BaseSelect from "./Select";
|
||||
import manageState from "./manageState";
|
||||
|
||||
export default manageState(Select);
|
||||
```
|
||||
|
||||
同理对异步的封装也可以放在 `makeAsync` 函数中:
|
||||
|
||||
```jsx
|
||||
const makeAsync = SelectComponent => ({
|
||||
getOptions,
|
||||
defaultOptions,
|
||||
...props
|
||||
}) => {
|
||||
const [options, setOptions] = useState(defaultOptions);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const onInputChange = async newValue => {
|
||||
setIsLoading(true);
|
||||
const newOptions = await getOptions(newValue);
|
||||
setIsLoading(false);
|
||||
setOptions(newOptions);
|
||||
};
|
||||
|
||||
return (
|
||||
<SelectComponent
|
||||
{...props}
|
||||
options={options}
|
||||
isLoading={isLoading}
|
||||
onInputChange={onInputChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
可以看到,`SelectComponent` 是一个完全受控的数据驱动的 UI,无论是 `manageState` 还是 `makeAsync` 都是对数据处理的拓展,所以这三者之间才可以融洽的组合:
|
||||
|
||||
```js
|
||||
import BaseSelect from "./Select";
|
||||
import manageState from "./manageState";
|
||||
import makeAsync from "./async";
|
||||
|
||||
export default manageState(Select);
|
||||
|
||||
export const AsyncSelect = manageState(makeAsync(Select));
|
||||
```
|
||||
|
||||
后面还有一些风格化、开源协作的思考,这里就不展开了,对这部分感兴趣的同学可以查看原视频了解更多。
|
||||
|
||||
### React + 政府财政透明项目
|
||||
|
||||
usaspending.gov 这个网站使用 React 建设,可以查看美国政府支持财政的明细,通过流畅的体验让更多用户可以了解国家财政支出,进一步推动财政支出的透明化。由于并不涉及前端技术的介绍,主要是产品介绍,因此精读就不详细展开了。
|
||||
|
||||
顺便说一句,智能分析数据就用 [QuickBI](https://www.alibabacloud.com/zh/product/quickbi),QuickBI 是我们团队研发的一款智能 BI 服务平台,如果你将美国政府的财政支持作为数据集输入,你会分析得更透彻。
|
||||
|
||||
### React + 星舰模拟器
|
||||
|
||||
最后介绍的是使用 React 制作的星舰模拟器,看上去像一个游戏:
|
||||
|
||||
<img width=500 src="https://img.alicdn.com/tfs/TB1HrxAneL2gK0jSZPhXXahvXXa-1946-1104.png">
|
||||
|
||||
有星系图、船体、驾驶员信息、武器装备、燃料、通信等等内容。甚至可以模拟太空驾驶,进行任务,可以实时多人协同。对太空迷们的吸引力很大,感兴趣的同学建议直接观看 [视频](https://youtu.be/JDDxR1a15Yo?t=28638)。
|
||||
|
||||
## 3 总结
|
||||
|
||||
第二天的内容非常全面,涉及了 React API、开发者周边、codemod 工具、代码维护、写作/音乐与代码、动画、函数式编程、看似简单的 React 组件、使用 React 制作的各种脑洞大开的项目,等等。
|
||||
|
||||
React Conf 要展示的是一个完整的 React 世界,第一天提到了 React 是一个桥梁,正因为这个桥梁,连接了各行各业不同的人群以及不同的项目,大家都有一个共同的语言:React。
|
||||
|
||||
"We not only react code, but react the world"。
|
||||
|
||||
> 讨论地址是:[精读《React Conf 2019 - Day2》 · Issue #217 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/217)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
|
||||
|
||||
> 关注 **前端精读微信公众号**
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
|
||||
|
||||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
|
||||
@@ -1,6 +1,6 @@
|
||||
# 1 引言
|
||||
|
||||
<img src="assets/13/logo.jpeg" width="500" alt="logo" />
|
||||
<img src="https://img.alicdn.com/imgextra/i2/O1CN014VGV7a1x3ILYqK9OD_!!6000000006387-2-tps-1024-732.png" width="500" alt="logo" />
|
||||
|
||||
javascript 的 this 是个头痛的话题,本期精读的文章更是引出了一个观点,避免使用 this。我们来看看是否有道理。
|
||||
|
||||
@@ -53,7 +53,7 @@ getName(person3) // Name: Sarah Doe
|
||||
getGreetingCallback(person3)('Jeff') // Hello Jeff, I'm Sarah Doe
|
||||
```
|
||||
|
||||
<img src="assets/13/1.png" width="500" alt="demo1" />
|
||||
<img src="https://img.alicdn.com/imgextra/i3/O1CN017Kw37u1oOyYHXGlqC_!!6000000005216-2-tps-1338-338.png" width="500" alt="demo1" />
|
||||
|
||||
这样 person 实例是个纯对象,没有将方法挂载到原型链上,简单易懂。
|
||||
|
||||
@@ -215,7 +215,7 @@ var bar = foo.bind(obj)
|
||||
bar()
|
||||
```
|
||||
|
||||
### 3.2.2 es6绑定
|
||||
### 3.2.2 es6 绑定
|
||||
|
||||
这种情况类似使用箭头函数创建成员变量,以下方式等于创建了没有挂载到原型链的匿名函数,因此 this 不会丢失。
|
||||
|
||||
@@ -0,0 +1,262 @@
|
||||
## 1 引言
|
||||
|
||||
搭配了合适的设计模式的代码,才可拥有良好的可维护性,[The Benefits of Orthogonal React Components](https://dmitripavlutin.com/orthogonal-react-components/) 这篇文章就重点介绍了正交性原理。
|
||||
|
||||
所谓正交,即模块之间不会相互影响。想象一个音响的音量与换台按钮间如果不是正交关系,控制音量同时可能影响换台,这样的设备很难维护:
|
||||
|
||||
<img width=400 src="https://img.alicdn.com/tfs/TB1dczIpQL0gK0jSZFtXXXQCXXa-1000-993.png">
|
||||
|
||||
前端代码也一样,UI 与数据处理逻辑分离就是一种符合正交原则的设计,这样有利于长期代码质量维护。
|
||||
|
||||
## 2 概述
|
||||
|
||||
一个拥有良好正交性的 React App 会按照如下模块分离设计:
|
||||
|
||||
1. UI 元素(展示型组件)。
|
||||
2. 取数逻辑(fetch library, REST or GraphQL)。
|
||||
3. 全局状态管理(redux)。
|
||||
4. 持久化(local storage, cookies)。
|
||||
|
||||
文中通过两个例子说明。
|
||||
|
||||
### 让组件与取数逻辑正交
|
||||
|
||||
比如一个展示雇员列表组件 `<EmployeesPage>`:
|
||||
|
||||
```jsx
|
||||
import React, { useState } from "react";
|
||||
import axios from "axios";
|
||||
import EmployeesList from "./EmployeesList";
|
||||
|
||||
function EmployeesPage() {
|
||||
const [isFetching, setFetching] = useState(false);
|
||||
const [employees, setEmployees] = useState([]);
|
||||
|
||||
useEffect(function fetch() {
|
||||
(async function() {
|
||||
setFetching(true);
|
||||
const response = await axios.get("/employees");
|
||||
setEmployees(response.data);
|
||||
setFetching(false);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
if (isFetching) {
|
||||
return <div>Fetching employees....</div>;
|
||||
}
|
||||
return <EmployeesList employees={employees} />;
|
||||
}
|
||||
```
|
||||
|
||||
这样设计看上去没问题,但其实违背了正交原则,因为 `EmployeesPage` 既负责渲染 UI 又关心取数逻辑。正交的写法如下:
|
||||
|
||||
```jsx
|
||||
import React, { Suspense } from "react";
|
||||
import EmployeesList from "./EmployeesList";
|
||||
|
||||
function EmployeesPage({ resource }) {
|
||||
return (
|
||||
<Suspense fallback={<h1>Fetching employees....</h1>}>
|
||||
<EmployeesFetch resource={resource} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
function EmployeesFetch({ resource }) {
|
||||
const employees = resource.employees.read();
|
||||
return <EmployeesList employees={employees} />;
|
||||
}
|
||||
```
|
||||
|
||||
**`Suspense` 将 loading 状态剥离到父级组件,因此子组件只需要关心如何用数据,不需关心如何取数据(以及 loading 态)。**
|
||||
|
||||
### 让组件与滚动监听正交
|
||||
|
||||
比如一个滚动到一定距离就出现 "jump to top" 的组件 `<ScrollToTop>`,可能会这么实现:
|
||||
|
||||
```jsx
|
||||
import React, { useState, useEffect } from "react";
|
||||
|
||||
const DISTANCE = 500;
|
||||
|
||||
function ScrollToTop() {
|
||||
const [crossed, setCrossed] = useState(false);
|
||||
|
||||
useEffect(function() {
|
||||
const handler = () => setCrossed(window.scrollY > DISTANCE);
|
||||
handler();
|
||||
window.addEventListener("scroll", handler);
|
||||
return () => window.removeEventListener("scroll", handler);
|
||||
}, []);
|
||||
|
||||
function onClick() {
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
behavior: "smooth"
|
||||
});
|
||||
}
|
||||
|
||||
if (!crossed) {
|
||||
return null;
|
||||
}
|
||||
return <button onClick={onClick}>Jump to top</button>;
|
||||
}
|
||||
```
|
||||
|
||||
可以看到,在这个组件中,按钮与滚动状态判断逻辑混合在了一起。如果我们将 “滚动到一定距离就渲染 UI” 抽象成通用组件 `IfScrollCrossed` 呢?
|
||||
|
||||
```jsx
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
function useScrollDistance(distance) {
|
||||
const [crossed, setCrossed] = useState(false);
|
||||
|
||||
useEffect(
|
||||
function() {
|
||||
const handler = () => setCrossed(window.scrollY > distance);
|
||||
handler();
|
||||
window.addEventListener("scroll", handler);
|
||||
return () => window.removeEventListener("scroll", handler);
|
||||
},
|
||||
[distance]
|
||||
);
|
||||
|
||||
return crossed;
|
||||
}
|
||||
|
||||
function IfScrollCrossed({ children, distance }) {
|
||||
const isBottom = useScrollDistance(distance);
|
||||
return isBottom ? children : null;
|
||||
}
|
||||
```
|
||||
|
||||
有了 `IfScrollCrossed`,我们就能专注写 “点击按钮跳转到顶部” 这个 UI 组件了:
|
||||
|
||||
```jsx
|
||||
function onClick() {
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
behavior: "smooth"
|
||||
});
|
||||
}
|
||||
|
||||
function JumpToTop() {
|
||||
return <button onClick={onClick}>Jump to top</button>;
|
||||
}
|
||||
```
|
||||
|
||||
最后将他们拼装在一起:
|
||||
|
||||
```jsx
|
||||
import React from "react";
|
||||
|
||||
// ...
|
||||
|
||||
const DISTANCE = 500;
|
||||
|
||||
function MyComponent() {
|
||||
// ...
|
||||
return (
|
||||
<IfScrollCrossed distance={DISTANCE}>
|
||||
<JumpToTop />
|
||||
</IfScrollCrossed>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
这么做,我们的 `<JumpToTop>` 与 `<IfScrollCrossed>` 组件就是正交关系,而且逻辑更清晰。不仅如此,这样的抽象使 `<IfScrollCrossed>` 可以被其他场景复用:
|
||||
|
||||
```jsx
|
||||
import React from "react";
|
||||
|
||||
// ...
|
||||
|
||||
const DISTANCE_NEWSLETTER = 300;
|
||||
|
||||
function OtherComponent() {
|
||||
// ...
|
||||
return (
|
||||
<IfScrollCrossed distance={DISTANCE_NEWSLETTER}>
|
||||
<SubscribeToNewsletterForm />
|
||||
</IfScrollCrossed>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Main 组件
|
||||
|
||||
上面例子中,`<MyComponent>` 就是一个 Main 组件,Main 组件封装一些脏逻辑,即它要负责不同模块的组装,而这些模块之间不需要知道彼此的存在。
|
||||
|
||||
一个应用会存在多个 Main 组件,它们负责拼装各种作用域下的脏逻辑。
|
||||
|
||||
### 正交设计的好处
|
||||
|
||||
- **容易维护:** 正交组件逻辑相互隔离,不用担心连带影响,因此可以放心大胆的维护单个组件。
|
||||
- **易读:** 由于逻辑分离导致了抽象,因此每个模块做的事情都相对单一,很容易猜测一个组件做的事情。
|
||||
- **可测试:** 由于逻辑分离,可以采取逐个击破的思路进行单测。
|
||||
|
||||
### 权衡
|
||||
|
||||
如果不采用正交设计,因为模块之间的关联导致应用最终变得难以维护。但如果将正交设计应用到极致,可能会多处许多不必要的抽象,这些抽象的复用仅此一次,造成过度设计。
|
||||
|
||||
## 3 精读
|
||||
|
||||
正交设计一定程度可以理解为合理抽象,完全不抽象与过度抽象都是不可取的,因此列举了四块需要抽象的要点:UI 元素、取数逻辑、全局状态管理、持久化。
|
||||
|
||||
全局状态管理注入到组件,就是一种正交的抽象模式,即组件不用关心数据从哪来,而直接使用数据,而数据管理完全交由数据流层管理。
|
||||
|
||||
取数逻辑往往是可能被忽略的一环,无论是像原文中直接关心到 `fetch` 方法的 UI 组件,还是利用取数工具库关心了 `loading` 状态:
|
||||
|
||||
```jsx
|
||||
import useSWR from "swr";
|
||||
|
||||
function Profile() {
|
||||
const { data, error } = useSWR("/api/user", fetcher);
|
||||
|
||||
if (error) return <div>failed to load</div>;
|
||||
if (!data) return <div>loading...</div>;
|
||||
return <div>hello {data.name}!</div>;
|
||||
}
|
||||
```
|
||||
|
||||
虽然将取数生命周期封装到自定义 hook `useSWR` 中,但 `error` 信息对 UI 组件来说就是一个脏数据:**这让这个 UI 组件不仅要渲染数据,还要担心取数是否会失败,或者是否在 loading 中。**
|
||||
|
||||
好在 Suspense 模式解决了这个问题:
|
||||
|
||||
```jsx
|
||||
import { Suspense } from "react";
|
||||
import useSWR from "swr";
|
||||
|
||||
function Profile() {
|
||||
const { data } = useSWR("/api/user", fetcher, { suspense: true });
|
||||
return <div>hello, {data.name}</div>;
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Suspense fallback={<div>loading...</div>}>
|
||||
<Profile />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
这样 `<Profile>` 只要专注于做数据渲染,而不用担心 `useSWR('/api/user', fetcher, { suspense: true })` 这个取数过程发生了什么、是否取数失败、是否在 `loading` 中。因为取数状态由 `Suspense` 管理,而取数是否意外失败由 `ErrorBoundary` 管理。
|
||||
|
||||
合理的抽象使组件逻辑变得更简单,从而组件嵌套使用使不用担心额外影响。尤其在大型项目中,不要担心正交抽象会使本来就很多的模块数量再次膨胀,因为相比于维护 100 个相互影响,内部逻辑复杂的模块,维护 200 个职责清晰,相互隔离的模块也许会更轻松。
|
||||
|
||||
## 4 总结
|
||||
|
||||
从正交设计角度来看,`Hooks` 解决了状态管理与 UI 分离的问题,`Suspense` 解决了取数状态与 UI 分离的问题,`ErrorBoundary` 解决了异常与 UI 分离的问题。
|
||||
|
||||
在你看来,React 还有哪些逻辑需要与 UI 分离?分别使用哪些方法呢?欢迎留言。
|
||||
|
||||
> 讨论地址是:[精读《正交的 React 组件》 · Issue #221 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/221)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
|
||||
|
||||
> 关注 **前端精读微信公众号**
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
|
||||
|
||||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
|
||||
@@ -0,0 +1,207 @@
|
||||
## 1 引言
|
||||
|
||||
[尤雨溪](https://github.com/yyx990803) 在 2019 JSConf 的分享 [Seeking the Balance in Framework Design](https://www.youtube.com/watch?v=ANtSWq-zI0s) 十分精彩,道出了如何进行合理的前端框架设计与框架选型。
|
||||
|
||||
正如所说,框架对比不能只停留在 Star 数量、Npm 下载量、Stackoverflow 问题量这些简单的数据对比,而要深入到技术细节进行比较。比较框架有多种不同维度,这次分享就从服务范围、渲染机制、状态机制这三个维度进行对比。
|
||||
|
||||
## 2 概述
|
||||
|
||||
这次分享的精彩之处在于不偏不倚的站在客观立场分析了框架各维度好的一面与坏的一面,从中我们不仅能学习到一些框架知识,还能培养思辨能力。
|
||||
|
||||
### 服务范围
|
||||
|
||||
服务范围是个比较难翻译的单词,在原 PPT 中用了 “Scope” 这个单词表示,可以理解为 “作用域、框架的承诺功能范围、服务配套齐全程度”。比如提供的是一个工具库还是整体框架,插件管理是集中式还是依赖生态。
|
||||
|
||||
React 是典型的小服务范围框架,核心包只实现了基本功能,而其他生态基本靠社区拓展;Angular 是典型大服务范围框架,官方对所有业务场景都做了最佳实践能力覆盖;Vue 处在中间区域,通过功能分层,既拥有小服务范围的能力,又可以搭配官方插件实现更多场景化能力。
|
||||
|
||||
#### 小服务范围优势
|
||||
|
||||
**概念少,易上手**
|
||||
|
||||
小的服务范围代表了小的学习成本,因为暴露的基本能力较少,概念也会比较少,对新人上手比较友好。
|
||||
|
||||
**生态繁荣,百花齐放**
|
||||
|
||||
由于很多功能没有被官方实现,社区就有机会填补这些空白,因此会冒出许多第三方库,而且一旦做得好,就有机会成为 “事实标准”,因此开发者会更加积极参与到社区开发,自己做的框架 “上升空间” 也非常大。
|
||||
|
||||
同时,社区的力量会导致多元化,因此整体生态完整度与创新性都会非常亮眼,而且具有持续迭代的能力。
|
||||
|
||||
**核心维护成本低**
|
||||
|
||||
官方维护的核心代码较少,因此维护成本大大降低,而且官方可以将精力放在更多核心能力增强上,比如 Suspense 等,而不是将精力消耗在生态插件上。
|
||||
|
||||
#### 小服务范围的劣势
|
||||
|
||||
**复杂场景要引入新概念**
|
||||
|
||||
复杂场景无法支持时,就要引入新的概念解决,这导致后续技术选型可能产生分歧,并带来持续的新概念理解成本。
|
||||
|
||||
**非官方的开发模式逐渐产生**
|
||||
|
||||
随着时间的流逝,会逐渐涌出一些新的设计模式,成为当下几乎是必不可少的方案,但却不会出现在官方文档中,造成选型时的疑惑。Redux 就是一个例子。
|
||||
|
||||
**生态变化快,碎片化且持续流失**
|
||||
|
||||
非官方的生态也意味着不稳定,而且缺乏统一的管理,碎片化的模块之间可能经常出现不兼容的问题。
|
||||
|
||||
而且任何模块都可能被时代无情的淘汰,就像 Flux 到 Redux 再到 Hooks,带来额外的迁移成本和认知成本。谁也不希望自己的项目架构 “变得过时”,或者随时面临被新架构取代的风险,但第三方社区几乎一定代表未来会出现一种模式取代现有模式,只是时间早晚而已。
|
||||
|
||||
#### 大服务范围的优势
|
||||
|
||||
**大部分业务场景都被内置解决**
|
||||
|
||||
减少不必要的技术方案调研与纷争,大服务范围的框架内置的方案就能解决几乎 100% 业务问题,团队再也不会为通用架构问题烦恼了。
|
||||
|
||||
**生态稳定、连贯**
|
||||
|
||||
稳定是指,官方维护作为背书,几乎不会存在一些生态包突然不维护、与已有版本不兼容、被植入恶意程序等等意外情况。
|
||||
|
||||
连贯是指,官方会统一考虑一个改动在所有生态插件造成的影响,并以一个最合理的思路做整体改造,生态包无论是接口还是兼容性都不需要担心,设计思路也会一脉相承。
|
||||
|
||||
#### 大服务范围的劣势
|
||||
|
||||
**前期上手成本高**
|
||||
|
||||
全家桶的概念导致上手难度偏高,因为必须理解所有内置概念后才能开始项目。
|
||||
|
||||
**如果内置模块无法满足业务,会觉得有些死板**
|
||||
|
||||
一旦发生内置功能无法满足业务的场景,就很难拓展了,因为 all in one 的思路本质上就是排斥自定义拓展的,这点从 [angular-cli](https://github.com/angular/angular-cli) 就能看出来。
|
||||
|
||||
之所以觉得死板,是因为这种情况没办法用优雅的方式解决,只能在现有约束的框架内通过某些 “Hack” 方式解决,自然会有种死板的感觉。
|
||||
|
||||
#### 中等服务范围的优势
|
||||
|
||||
**分层设计,允许新特性渐进加入**
|
||||
|
||||
Vue 通过分层设计做到了折中,即官方还是会维护生态,只不过生态不是必须的,可以按需使用。这样做的好处是兼顾了一些优势。
|
||||
|
||||
**低学习门槛**
|
||||
|
||||
与小服务范围框架一样,对于核心包来说学习成本都比较低。
|
||||
|
||||
**依然有最佳实践解决所有业务问题**
|
||||
|
||||
和大服务范围框架一样,拥有全套官方最佳实践,但不内置,不强求一定要使用,因此你可以按需使用。
|
||||
|
||||
#### 中等服务范围的劣势
|
||||
|
||||
**维护成本高**
|
||||
|
||||
和大服务范围框架一样,虽然生态不强求,但毕竟官方还是要持续维护的,因此维护成本高的问题依然存在。
|
||||
|
||||
**生态多样性不高**
|
||||
|
||||
虽然生态是按需的,但毕竟中等服务范围的框架官方会实现一套标准生态插件,这会极大影响社区生态的发展空间,导致 “非官方插件没人愿意做”,因此生态多样性会差一些。
|
||||
|
||||
### 渲染机制
|
||||
|
||||
渲染机制区别主要在 JSX vs Template 之间,不同的表达方式之间还是存在一些很本质的区别,然而正如一开始所说,无法一言蔽之,必须从多个角度拆解的看。
|
||||
|
||||
#### JSX 的优势
|
||||
|
||||
**纯 JS 表达 UI**
|
||||
|
||||
单这一点就非常重要了,满足了 All In Js 的幻想。毕竟 Html、Css 相比 Js 来说,模块化能力和灵活性都很弱,将其都收敛到 Js 不仅表达方式更统一,更重要的是都获得了与 Js 一样的模块化、灵活性、Typescript 支持等能力。
|
||||
|
||||
**视图即数据**
|
||||
|
||||
将视图看作一种数据,让针对视图的逻辑测试成为可能。
|
||||
|
||||
同时也将视图概念泛化了,因为数据是平台无关的,一份描述视图的 DSL 可以运行在任何平台。
|
||||
|
||||
#### JSX 的劣势
|
||||
|
||||
**开销大**
|
||||
|
||||
页面节点越多,Diff 开销就越大。
|
||||
|
||||
**动态渲染很难性能优化**
|
||||
|
||||
由于所有 DOM 节点都是动态生成,因此无法根据初始状态结构进行安全的优化。相比之下,Template 模式可以确定哪部分属于变量,哪部分是固定的,对固定部分的 Diff 检测都可以跳过。
|
||||
|
||||
**动态调度虽然改善了性能,但依赖更重的运行时**
|
||||
|
||||
React ConcurrentMode 是一个调度优化器,但实现的逻辑也比较复杂,加重了运行时负担。
|
||||
|
||||
#### Template 的优势
|
||||
|
||||
**原生性能**
|
||||
|
||||
由于 Template 对节点进行直接渲染,因此与原生性能一致。
|
||||
|
||||
**Runtime 更小**
|
||||
|
||||
由于不需要额外优化,运行时代码会小很多。
|
||||
|
||||
#### Template 的劣势
|
||||
|
||||
**被 Template 语法约束,且无法拓展**
|
||||
|
||||
对于 Template 不支持的,只能选择接受,因为除了框架自己,没有人能拓展 Template 的特性。当遇到一些非常动态场景,但 Template 不支持的情况,只能选择接受,并用比较 Hack 的方式绕过解决,除此之外别无他法。
|
||||
|
||||
**模版冗长**
|
||||
|
||||
JSX 可以利用循环语句或者变量赋值进行模版区块的复用,但 Template 模式每次新模版都要一行一行的打出来,这种冗长的开发体验不太友好。
|
||||
|
||||
**运行时解析开销或者依赖编译期逻辑**
|
||||
|
||||
要么通过编译器预先生成 AST,要么运行时动态将 Template 解析成 AST,无论哪种方案都有额外的开销,一种是工程依赖的开销,一种是运行时动态解析的性能开销。
|
||||
|
||||
#### VDom + Template 的特色
|
||||
|
||||
Vue 在 Template 基础上支持了虚拟 DOM,因此兼具两者特色。
|
||||
|
||||
性能上,在编译时就进行 AST 解析,减少了运行时解析开销。
|
||||
|
||||
功能上,支持模版与 JSX 两种语法。
|
||||
|
||||
### 状态机制
|
||||
|
||||
状态机制 [尤雨溪](https://github.com/yyx990803) 在 JSConf 提到要单独拆出来讲,因为内容较多,时间可能不够,本次精读也限于篇幅原因略过:
|
||||
|
||||
- Mutable vs Immutable。
|
||||
- 依赖追踪 vs 脏检测。
|
||||
- 响应式 vs 模拟响应式。
|
||||
|
||||
显然,状态机制方案更是仁者见仁智者见智的事情,同样得从多个维度进行独立分析,并根据实际业务场景具体选择。
|
||||
|
||||
最后,意识到没有一个绝对均衡的框架设计方案,因为在工程领域,没有最好只有更好。
|
||||
|
||||
## 3 精读
|
||||
|
||||
我们再延伸谈一谈为什么框架设计要寻找平衡点。
|
||||
|
||||
**框架设计没有银弹**
|
||||
|
||||
与数学公式不同,框架设计甚至整个工程技术设计都没有所谓的真理,所谓条条大路通罗马,实现同一个技术目标的众多方案之间也许就是平行关系,可以根据不同维度列出一二三的对比,但无法得出一个总的结论,孰优孰劣。
|
||||
|
||||
**使用场景不同**
|
||||
|
||||
不同使用场景决定了对框架诉求的不同。
|
||||
|
||||
比如开发非常定制、炫酷的可视化大屏,那么前端开发框架基本也用不上,因为关注点不会聚焦在项目路由、UI 描述、甚至是数据流,而是聚焦在性能、图形渲染等问题。解决这些领域的框架可能是 虚幻 4、Unity 等游戏引擎,但普通的前端开发框架绝不会涉足这种领域,框架一定要确定自己功能范围。
|
||||
|
||||
即便仅局限在 Web 领域,也需要考虑是否要支持非 Web 场景,那么将 HTML 抽象成一个通用 DSL 就可能是一种选择,但非 Web 领域毕竟不是主打业务领域,在这种业务场景周边生态维护可能就比较少,这也是需要取舍的地方。
|
||||
|
||||
**使用的人不同**
|
||||
|
||||
不同团队对框架的要求也不同。
|
||||
|
||||
刚起步的小团队可能更需要保姆式的框架,因为这样最节省人力成本。对于规模较大的团队,希望对框架拥有较大定制能力时,小服务范围的框架可能更受青睐。当然框架作者可以像 Vue 一样做出渐进式官方能力增强方案,以此满足不同需求的用户,但毕竟也不能将生态完全交给社区,还是要做取舍。
|
||||
|
||||
所以当遇到更新更酷的框架时,需要冷静思考的不只是这个框架带来的收益与花费的迁移成本哪个更高,以及团队能否接受这套框架的开发习惯,更需要思考的是这个框架自身做了哪些权衡,如果这些权衡与 React、Vue、Angular 类似,那么仅仅变化了语法或者语言的改动其实意义不大,此时需要慎重考虑。
|
||||
|
||||
## 4 总结
|
||||
|
||||
这次没有提到的状态机制对比,你能分别列举出优缺点吗?欢迎留言。
|
||||
|
||||
> 讨论地址是:[精读《寻找框架设计的平衡点》 · Issue #223 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/223)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
|
||||
|
||||
> 关注 **前端精读微信公众号**
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
|
||||
|
||||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
|
||||
@@ -0,0 +1,111 @@
|
||||
## 1 引言
|
||||
|
||||
当下互联网行业里面最流行的就是 ABC:
|
||||
|
||||
> A: AI 人工智能 B: BIG DATA C: CLOUD
|
||||
|
||||
而阿里经济体中的 ABC,其中的 BIG DATA,即是我们 DT https://dt.alibaba.com/ ,我们用大数据赋能商业,创造价值。
|
||||
|
||||
而我们说数据中台,其实阿里提出的中台只有两个:业务中台与数据中台。业务中台的目的是让业务能够快速落地,数据中台的目的是完成数据的采集、建设、管理、使用这四个环节,让数据从生产到使用过程变得丝般顺滑,不仅不让数据资产成为累赘,还会最大限度发挥出数据潜藏的价值。
|
||||
|
||||
笔者所在的就是数据中台的大前端团队,既为阿里经济体提供数据服务,又着力为上云企业打造属于自己的数据中台,处在前端技术、商业模式、产品设计的最前沿,且听我慢慢道来。
|
||||
|
||||
## 2 精读
|
||||
|
||||
### 全链路数据能力
|
||||
|
||||
从能力上看,数据中台处理数据的方方面面,从数据产生开始就进行追踪,不仅打通了数据采集、存储、处理、查询、消费的全链路,还用以下几种方式赋能业务:研发数据管理平台并监控数据质量,研发生意参谋等数据分析产品直接服务大、中、小商家,提供统一数据服务标准化数据使用流程,将数据分析的算法能力服务化,将支撑内部的数据服务上云搭建客户自己的数据中台,研发 BI 平台完成数据决策的最后一环。
|
||||
|
||||
### 全链路数据技术
|
||||
|
||||
从技术架构上看,从底层的数据采集技术开始,逐步向上建设了数据计算与管理能力、数据服务、数据平台、数据应用与数据安全。
|
||||
|
||||
从使用者角度来看,现在的公司对数据的诉求可以概括为以下几点:
|
||||
|
||||
1. 数据从哪来,如何完全数字化:对应全链路数据采集服务。
|
||||
2. 如何得到想要的数据:数据计算、建模与管理服务。
|
||||
3. 如何使用数据:统一数据服务平台。
|
||||
4. 如何利用数据做商业决策:BI 平台。
|
||||
5. 如何保障数据安全:数据安全服务。
|
||||
|
||||
对阿里而言,还会额外考虑下面几点:
|
||||
|
||||
1. 如何让数据服务横向支撑所有业务线:数据服务平台化,数据智能化服务平台与 BI 平台。
|
||||
2. 如何让数据服务普惠到每一个企业:数据服务全面上云。
|
||||
3. 如何让数据服务更有价值:打通阿里经济体的数据体系,让数据相互产生化学反应。
|
||||
|
||||
当然,挑战性也非常大,首先是数据壁垒的挑战,要说服其他团队将数据交给你管理绝非易事。其次是价值挑战,如何证明数据中台存在的价值,并做到肉眼可见的业务增值。最后是技术挑战,对前端来说,几十款数据产品的搭建、几十万张数据报表的搭建,需要一个足够好用的数据产品搭建平台来支持;数据分析产品的下一代探索式分析也对 BI 引擎提出了新的要求;数据可视化远比普通可视化复杂,不仅要考虑大数据下的性能与可读性,还要理解商业,做出能体现数据分析价值的图表。
|
||||
|
||||
不论是数据搭建还是数据可视化,都是前端垂直领域的另一条好赛道,不仅有沉甸甸的业务价值,还有全新数据领域的的前端技术挑战,而且随着数据中台影响力的持续扩大,我们的前端技术也会带来业界越来越大的影响力。
|
||||
|
||||
### 如何建设和管理数据
|
||||
|
||||
想要数据用的好,首先要管的好,在大数据时代,企业必须建立一套自己的标准数仓系统对数据的采集、运维调度做全链路管理,让大数据变成好数据,让好数据可以发挥价值。
|
||||
|
||||

|
||||
|
||||
> Dataphin 数仓建设平台。
|
||||
|
||||
数仓的建设需要从物理空间与逻辑空间,也就是底层的表开始整理,通过对数据的采集、清洗、结构化,产出一套规范的数据定义。
|
||||
|
||||
所谓规范的数据定义即口径、算法、命名均一致的数据规范,降低数据二义性,提升数据查找效率与准确性。之后对数据建模,建模即是对数据的进一步抽象,可能是抽象为一个 Cube 模型,这样在顶层认知上,所有数据都是不同维度的 Cube,方便统一理解。
|
||||
|
||||
最后通过对数据进行在线的、离线的调度计算,产出数据资产。
|
||||
|
||||
### 如何看数据
|
||||
|
||||
或导出一个 Excel 文件仔细品味,或如双十一媒体大屏般夺目,或如股票操盘手般紧盯着屏幕,或随时随地的手机浏览。在哪看,怎么看,看什么,决定着同一份数据可带来不同的效果,产生不同的价值。
|
||||
|
||||
稳:双十一大屏,零点起得来,24 点收得住,每个彩蛋的出现,每个数字的跳动,如丝般顺滑,这不是播放 VCR,每一帧画面都是真实的数据展现。容:即是生意参谋用户的浏览器兼容,又是多端用户的兼容,也是 BI 分析结果的数据大容量。有容乃大,方显前端功底。
|
||||
|
||||
**“如何看数据” 这恰是做为数据前端人的使命和责任。** 不同的人,不同的端,不同的需求,这恰是给数据前端的挑战。而让用户透过数据创造价值,也正是数据前端人的价值。
|
||||
|
||||
### 如何分析数据
|
||||
|
||||
大数据浪潮之下,必然会诞生各式各样的数据产品,产品化的方式可以降低数据应用的门槛。我们希望人人都能成为数据分析师,于是 BI (商业智能)产品应运而生,作为大数据行业中的一个重要领域,BI 产品用大数据的方式解决了企业的业务分析需求,支撑企业进行数字化转型,从经验驱动决策转变为数据驱动决策,进而给企业带来超额收益。
|
||||
|
||||

|
||||
|
||||
> QuickBI 数据分析工具。
|
||||
|
||||
**人人都是数据分析师的情况在不断增强。**
|
||||
|
||||
根据 Gartner 对 2020 年 BI 产品发展趋势预测:
|
||||
|
||||
1. 到 2020 年,为用户提供对内部和外部数据策划目录的访问权限的组织将从分析投资中获得两倍的业务价值。
|
||||
2. 到 2020 年,业务部门的数据和分析专家数量的增速将是 IT 部门专家的 3 倍,这会迫使企业重新考虑其组织模式和技能。
|
||||
3. 到 2021 年,自然语言处理和会话分析这两个功能,会在新用户、特别是一线工作人员中,将分析和商业智能产品的使用率从 35% 提升到 50% 以上。
|
||||
|
||||
**快速增涨的市场规模。**
|
||||
|
||||
根据中国电子信息产业发展研究院发布的《中国大数据产业发展水平评估报告》,预计 2019 年我国大数据核心产业规模突破 5700 亿元,未来 2-3 年的市场规模的增长率仍将保持 35% 左右。未来切入这部分应用环节,BI 商业智能的潜在市场规模将在数百亿的市场空间。
|
||||
|
||||
**大数据与前端。**
|
||||
|
||||
前端的职业发展除了提升自己的技能技术储备之外,选择合适行业方向和研究领域也尤为重要。如果用路和车的关系来比喻的话,把前端技能比作车的话,各个行业都是路,有的路是乡间小路,有的路是城乡公路,而大数据行业当之无愧是行业中的上高速公路,路况更好,路面更宽,如果你拥有一辆好车,为什么不来高速公路上飞驰呢?
|
||||
|
||||
大数据下的前端面临哪些挑战?以 BI 为例,BI 领域的四大方向:数据集、渲染引擎、数据模型与可视化都有许多可以做深的技术点,每一块都需要深入沉淀几年技术经验才能做好,需要大量优秀人才通力协作才有可能做好。你也可以阅读 [精读《前端与 BI》](https://github.com/dt-fe/weekly/blob/v2/121.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%89%8D%E7%AB%AF%E4%B8%8E%20BI%E3%80%8B.md) 了解更多 BI 相关知识。
|
||||
|
||||
### 我们是数据中台大前端
|
||||
|
||||
> “ 前端不是因为我们用 JavaScript,而是因为我们站在业务最前端,解决业务端的问题,所以我们是前端 ”。
|
||||
|
||||
BI 分析产品、做数据可视化、做产品搭建 .. 我们早已经跳出了“前端”的传统概念范畴。我们做大数据表格优化、 Web Excel、 SQL 编辑器、智能可视化。在数据中台,我们有着天然的复杂业务场景和海量数据优势,迫使你向自己提出更大的挑战来解决业务上的问题。如果你热爱挑战、热爱技术,请加入我们吧。
|
||||
|
||||
**在这里,你可以愉快的使用 React、TypesScript 写业务代码,尝试最新、最炫酷的 React Hooks 新特性,我们团队一直走在前端技术路线的最前沿,渴求技术创新。** 你也不需要担心伙伴的代码风格问题,因为我们有着严格的代码规;你不必担心每个人的代码都是一座孤岛,因为我们会对每一行代码做严格的 review;你不必担心你的成长空间,我们有定期的技术分享、团队内小竞赛,还有足够复杂的业务场景支撑;你也不必担心你会因工作日渐消瘦,下午茶和海量小零食等你来!
|
||||
|
||||
## 4 总结
|
||||
|
||||
**大数据前端人才缺口在 100 人以上,由于业务增长非常非常迅猛,春节前条件放宽、特批急召!**
|
||||
|
||||
如果你对我们感兴趣,请立刻把简历发送到邮箱 **ziyi.hzy@alibaba-inc.com** 吧!绝无仅有的好机会,响应速度绝对超乎你的想象!
|
||||
|
||||
> 讨论地址是:[精读《我在阿里数据中台大前端》 · Issue #224 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/224)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
|
||||
|
||||
> 关注 **前端精读微信公众号**
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
|
||||
|
||||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
|
||||
@@ -0,0 +1,123 @@
|
||||
## 1 引言
|
||||
|
||||
本周精读的文章是 [Mastering JS console.log like a Pro](https://medium.com/javascript-in-plain-english/mastering-js-console-log-like-a-pro-1c634e6393f9),一起来更全面的认识 console 吧!
|
||||
|
||||
## 2 概述 & 精读
|
||||
|
||||
console 的功能主要在于控制台打印,它可以打印任何字符、对象、甚至 DOM 元素和系统信息,下面一一介绍。
|
||||
|
||||
### console.log( ) | info( ) | debug( ) | warn( ) | error( )
|
||||
|
||||
直接打印字符,区别在于展示形态的不同:
|
||||
|
||||
<img width=400 src="https://img.alicdn.com/tfs/TB1xZ_WveH2gK0jSZFEXXcqMpXa-1492-566.png">
|
||||
|
||||
新版 chrome 控制台可以将打印信息分类:
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB1fZ2Vvhn1gK0jSZKPXXXvUXXa-420-446.png">
|
||||
|
||||
`log()` 与 `info()` 都对应 `info`,`warn()` 对应 `warnings`,`error()` 对应 `errors`,而 `debug()` 对应 `verbose`,因此建议在合适的场景使用合适的打印习惯,这样排查问题时也可以有针对性的筛选。
|
||||
|
||||
比如调试信息可以用 `console.debug` 仅在调试环境下输出,调试者即便开启了调试参数也不会影响正常 `info` 的查看,因为调试信息都输出在 `verbose` 中。
|
||||
|
||||
### 使用占位符
|
||||
|
||||
- %o — 对象
|
||||
- %s — 字符串
|
||||
- %d — 数字
|
||||
|
||||
如下所示,可通过占位符在一行中插入不同类型的值:
|
||||
|
||||
<img width=400 src="https://img.alicdn.com/tfs/TB1GtL3vlr0gK0jSZFnXXbRRXXa-1840-504.png">
|
||||
|
||||
### 添加 CSS 样式
|
||||
|
||||
- %c - 样式
|
||||
|
||||
<img width=400 src="https://img.alicdn.com/tfs/TB1eK23vlr0gK0jSZFnXXbRRXXa-1832-978.png">
|
||||
|
||||
可以总结出,**console 支持输出复杂的内容,其输出能力堪比 HTML,但输入能力太弱,仅为字符串,因此采用了占位符 + 多入参修饰的设计模式解决这个问题。**
|
||||
|
||||
### console.dir( )
|
||||
|
||||
按 JSON 模式输出。笔者在这里也补充一句:`console.log()` 会自动判断类型,如果内容是 DOM 属性,则输出 DOM 树,但 `console.dir` 会强制以 JSON 模式输出,用在 DOM 对象时可强制转换为 JSON 输出。
|
||||
|
||||
<img width=400 src="https://img.alicdn.com/tfs/TB1KQY1vbj1gK0jSZFuXXcrHpXa-922-302.png">
|
||||
|
||||
### 输出 HTML 元素
|
||||
|
||||
按照 HTML ELements 结构输出:
|
||||
|
||||
<img width=400 src="https://img.alicdn.com/tfs/TB1mZ61va61gK0jSZFlXXXDKFXa-920-255.png">
|
||||
|
||||
这种输出结构和 Elements 打印形式是一致的,如果要看详细属性,可以使用 `console.dir()`。
|
||||
|
||||
### console.table
|
||||
|
||||
在控制台打印一个表格,属于功能增强。虽然仅文本也可以在控制台打印出漂亮的表格,但浏览器调试控制台的功能更强大,`console.table` 只是其富文本能力的一个体现。
|
||||
|
||||
<img width=400 src="https://img.alicdn.com/tfs/TB1WldouKbviK0jSZFNXXaApXXa-928-742.png">
|
||||
|
||||
### console.group( ) & console.groupEnd( )
|
||||
|
||||
接下来是另一个富文本能力,按分组输出:
|
||||
|
||||
<img width=400 src="https://img.alicdn.com/tfs/TB1UV6UvXY7gK0jSZKzXXaikpXa-919-377.png">
|
||||
|
||||
这种带有副作用的 API 显然是为方便阅读而设计的,然而在需要输出大量动态结构化数据的场景下,还需要进行结构转换,是比较麻烦的地方。
|
||||
|
||||
### console.count( )
|
||||
|
||||
`count()` 用来打印调用次数,一般用在循环或递归函数中。接收一个 `label` 参数以定制输出,默认直接输出 `1 2 3` 数字。
|
||||
|
||||
<img width=400 src="https://img.alicdn.com/tfs/TB1ELLVveL2gK0jSZPhXXahvXXa-917-500.png">
|
||||
|
||||
### console.assert( )
|
||||
|
||||
`console` 版断言工具,当且仅当第一个参数值为 `false` 时才打印第二个参数作为输出。
|
||||
|
||||
<img width=400 src="https://img.alicdn.com/tfs/TB1HEDUvfb2gK0jSZK9XXaEgFXa-1842-548.png">
|
||||
|
||||
这种输出结果为 error,所以也可被 `console.error` + 代码级别断言所取代。
|
||||
|
||||
### console.trace( )
|
||||
|
||||
打印此时的调用栈,在打印辅助调试信息时非常有用。
|
||||
|
||||
<img width=400 src="https://img.alicdn.com/tfs/TB1Jh_YvkL0gK0jSZFAXXcA9pXa-1840-1096.png">
|
||||
|
||||
### console.time( )
|
||||
|
||||
打印代码执行时间,性能优化和监控场景比较常见。
|
||||
|
||||
<img width=400 src="https://img.alicdn.com/tfs/TB1wAT2vbj1gK0jSZFuXXcrHpXa-1612-524.png">
|
||||
|
||||
### console.memory
|
||||
|
||||
打印内存使用情况。
|
||||
|
||||
<img width=400 src="https://img.alicdn.com/tfs/TB1tPHYvkL0gK0jSZFAXXcA9pXa-1842-440.png">
|
||||
|
||||
### console.clear( )
|
||||
|
||||
清空控制台输出。
|
||||
|
||||
## 3 总结
|
||||
|
||||
`console` 提供了如此多的输出规范,其实也是在变相制定开发规范,毕竟离开发者最近的就是调试控制台,如果你的项目打印规范与标准规范有差异,那么调试时信息看起来就会很别扭。
|
||||
|
||||
可以看到,大部分开源库都良好的遵循了这套规范,比如三方库绝不会输出 `log()`,而且将错误、警告与调试信息正确分开,并尽量少的用 CSS 样式、分组、`table` 等功能,因为这些功能干扰性较强,不能保证所有用户都可接受。
|
||||
|
||||
相对的,项目源码就比较适合使用一些醒目的自定义规范,只要这套规则能被很好的执行起来。
|
||||
|
||||
最后留下一个讨论点:`console` 可以作为调试、招聘信息、隐藏菜单的投放点,你还看到过哪些有意思的 `console` 使用方式呢?欢迎留言。
|
||||
|
||||
> 讨论地址是:[精读《精通 console.log》 · Issue #228 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/228)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
|
||||
|
||||
> 关注 **前端精读微信公众号**
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
|
||||
|
||||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
|
||||
@@ -0,0 +1,279 @@
|
||||
## 1 引言
|
||||
|
||||
`JSON.parse` 是浏览器内置的 API,但如果面试官让你实现一个怎么办?好在有人已经帮忙做了这件事,本周我们一起精读这篇 [JSON Parser with Javascript](https://lihautan.com/json-parser-with-javascript/) 文章吧,再温习一遍大学时编译原理相关知识。
|
||||
|
||||
## 2 概述 & 精读
|
||||
|
||||
要解析 JSON 首先要理解语法概念,之前的 [精读《手写 SQL 编译器 - 语法分析》](https://github.com/dt-fe/weekly/blob/v2/066.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%89%8B%E5%86%99%20SQL%20%E7%BC%96%E8%AF%91%E5%99%A8%20-%20%E8%AF%AD%E6%B3%95%E5%88%86%E6%9E%90%E3%80%8B.md) 系列也有介绍过,不过本文介绍的更形象,看下面这个语法图:
|
||||
|
||||
<img width=400 src="https://img.alicdn.com/tfs/TB1EbjfvQL0gK0jSZFtXXXQCXXa-1837-857.png">
|
||||
|
||||
这是关于 Object 类型的语法描述图,从左向右看,根据箭头指向只要能走出这个迷宫就属于正确语法。
|
||||
|
||||
比如第一行 `{` → `whitespace` → `}` 表示 `{ }` 属于合法的 JSON 语法。
|
||||
|
||||
再比如观察向下的一条最长路线:`{` → `whitespace` → `string` → `whitespace` → `:` → `value` → `}` 表示 `{ string : value }` 属于合法的 JSON 语法。
|
||||
|
||||
你可能会问,双引号去哪儿了?这就是语法树最核心的概念了,这张图是关于 Object 类型的 **产生式**,同理还有 string、value 的产生式,产生式中可以嵌套其他产生式,甚至形成环路,以此拥有描述纷繁多变语法的能力。
|
||||
|
||||
最后我们再看一个环路,即 `{` → `whitespace` → `string` ... `,` → `whitespace` → `string` ... `,` ... `}`,我们发现,只要不走回头路,这条路是可以一直 “绕圈” 下去的,因此 Object 类型拥有了任意数量子字段的能力,只是每形成一个子字段,必须经过 `,` 号分割。
|
||||
|
||||
### 实现 Parser
|
||||
|
||||
首先实现一个基本结构:
|
||||
|
||||
```js
|
||||
function fakeParseJSON(str) {
|
||||
let i = 0;
|
||||
// TODO
|
||||
}
|
||||
```
|
||||
|
||||
`i` 表示访问字符的下标,当 `i` 走到字符串结尾表示遍历结束。
|
||||
|
||||
然后是下一步,用几个函数描述解析语法的过程:
|
||||
|
||||
```js
|
||||
function fakeParseJSON(str) {
|
||||
let i = 0;
|
||||
function parseObject() {
|
||||
if (str[i] === '{') {
|
||||
i++;
|
||||
skipWhitespace();
|
||||
|
||||
// if it is not '}',
|
||||
// we take the path of string -> whitespace -> ':' -> value -> ...
|
||||
while (str[i] !== '}') {
|
||||
const key = parseString();
|
||||
skipWhitespace();
|
||||
eatColon();
|
||||
const value = parseValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
其中 `skipWhitespace` 表示匹配并跳过空格,所谓匹配意味着匹配成功,此时 `i` 下标可以继续后移,否则匹配失败。下一步则判断如果 `i` 不是结束标志 `}`,则按照 `parseString` 匹配字符串 → `skipWhitespace` 跳过空格 → `eatColon` 吃掉冒号 → `parseValue` 匹配值,这个链路循环。其中吃掉冒号表示 “匹配冒号但不会产生任何结果,所以就像吃掉了一样”,吃这个动作还可以用在其他场景,比如吃掉尾分号。
|
||||
|
||||
> 对于看到这儿的小伙伴,笔者要友情提示一下,原文的思路是一种定制语法解析思路,无论是 `eatColon` 还是 `parseValue` 都仅具备解析 JSON 的通用性,但不具备解析任意语法的通用性。如果你想做一个具备解析任何通用语法的解析器,读入的内容应该是语法描述,处理方式必须更加通用,如果感兴趣可以阅读 [精读《手写 SQL 编译器 - 语法分析》](https://github.com/dt-fe/weekly/blob/v2/066.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%89%8B%E5%86%99%20SQL%20%E7%BC%96%E8%AF%91%E5%99%A8%20-%20%E8%AF%AD%E6%B3%95%E5%88%86%E6%9E%90%E3%80%8B.md) 系列文章了解更多。
|
||||
|
||||
由于 Object 第一个元素前面不允许加逗号,因此可以利用 `initial` 做一个初始化判定,在初始时机不会吃掉逗号:
|
||||
|
||||
```js
|
||||
function fakeParseJSON(str) {
|
||||
let i = 0;
|
||||
function parseObject() {
|
||||
if (str[i] === '{') {
|
||||
i++;
|
||||
skipWhitespace();
|
||||
|
||||
let initial = true;
|
||||
// if it is not '}',
|
||||
// we take the path of string -> whitespace -> ':' -> value -> ...
|
||||
while (str[i] !== '}') {
|
||||
if (!initial) {
|
||||
eatComma();
|
||||
skipWhitespace();
|
||||
}
|
||||
const key = parseString();
|
||||
skipWhitespace();
|
||||
eatColon();
|
||||
const value = parseValue();
|
||||
initial = false;
|
||||
}
|
||||
// move to the next character of '}'
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
那么当第一个子元素前面存在逗号时,由于没有 “吃掉逗号” 这个功能,所以读到逗号会报错,语法解析提前结束。
|
||||
|
||||
吃逗号和吃冒号的代码都非常简单,即判断当前字符串必须是 “要吃的那个元素”,并且在吃掉后将 `i` 下标自增 1:
|
||||
|
||||
```js
|
||||
function fakeParseJSON(str) {
|
||||
// ...
|
||||
function eatComma() {
|
||||
if (str[i] !== ',') {
|
||||
throw new Error('Expected ",".');
|
||||
}
|
||||
i++;
|
||||
}
|
||||
|
||||
function eatColon() {
|
||||
if (str[i] !== ':') {
|
||||
throw new Error('Expected ":".');
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
在有了基本判定功能后,`fakeParseJSON` 需要返回 Object,因此我们只需在每个循环中对 Object 赋值,最后一并 return 即可:
|
||||
|
||||
```js
|
||||
function fakeParseJSON(str) {
|
||||
let i = 0;
|
||||
function parseObject() {
|
||||
if (str[i] === '{') {
|
||||
i++;
|
||||
skipWhitespace();
|
||||
|
||||
const result = {};
|
||||
|
||||
let initial = true;
|
||||
// if it is not '}',
|
||||
// we take the path of string -> whitespace -> ':' -> value -> ...
|
||||
while (str[i] !== '}') {
|
||||
if (!initial) {
|
||||
eatComma();
|
||||
skipWhitespace();
|
||||
}
|
||||
const key = parseString();
|
||||
skipWhitespace();
|
||||
eatColon();
|
||||
const value = parseValue();
|
||||
result[key] = value;
|
||||
initial = false;
|
||||
}
|
||||
// move to the next character of '}'
|
||||
i++;
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
解析 Object 的代码就完成了。
|
||||
|
||||
接着试着解析 Array,下面是 Array 的语法图:
|
||||
|
||||
<img width=400 src="https://img.alicdn.com/tfs/TB1FvYjvKH2gK0jSZFEXXcqMpXa-1837-479.png">
|
||||
|
||||
我们只需要吃逗号和 `parseValue` 即可:
|
||||
|
||||
```js
|
||||
function fakeParseJSON(str) {
|
||||
// ...
|
||||
function parseArray() {
|
||||
if (str[i] === '[') {
|
||||
i++;
|
||||
skipWhitespace();
|
||||
|
||||
const result = [];
|
||||
let initial = true;
|
||||
while (str[i] !== ']') {
|
||||
if (!initial) {
|
||||
eatComma();
|
||||
}
|
||||
const value = parseValue();
|
||||
result.push(value);
|
||||
initial = false;
|
||||
}
|
||||
// move to the next character of ']'
|
||||
i++;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
接下来到了有趣的 `value` 语法图,可以看到 `value` 是许多种基础类型的 “或” 关系组成的:
|
||||
|
||||
<img width=400 src="https://img.alicdn.com/tfs/TB1uGrmvND1gK0jSZFyXXciOVXa-1836-1293.png">
|
||||
|
||||
我们只需要继续拆解分析即可:
|
||||
|
||||
```js
|
||||
function fakeParseJSON(str) {
|
||||
// ...
|
||||
function parseValue() {
|
||||
skipWhitespace();
|
||||
const value =
|
||||
parseString() ??
|
||||
parseNumber() ??
|
||||
parseObject() ??
|
||||
parseArray() ??
|
||||
parseKeyword('true', true) ??
|
||||
parseKeyword('false', false) ??
|
||||
parseKeyword('null', null);
|
||||
skipWhitespace();
|
||||
return value;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
其中 `parseKeyword` 函数用来解析一些保留关键字,比如将 `"true"` 解析成布尔类型 `true`:
|
||||
|
||||
```js
|
||||
function fakeParseJSON(str) {
|
||||
// ...
|
||||
function parseKeyword(name, value) {
|
||||
if (str.slice(i, i + name.length) === name) {
|
||||
i += name.length;
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
如上所示,只要在 name 与对应字符相等时,返回第二个传入参数即可。
|
||||
|
||||
### 处理异常输入
|
||||
|
||||
一个完整的语法解析功能需要包含错误处理,错误的情况主要分两种:
|
||||
|
||||
1. 非法字符。
|
||||
2. 非正常结尾。
|
||||
|
||||
原文提到的 JSON 错误提示优化非常棒,想想你在开发中突然看到下面的提示,是不是很蒙圈:
|
||||
|
||||
```text
|
||||
Unexpected token "a"
|
||||
```
|
||||
|
||||
既然我们是自己写的 JSON 解析器,就可以进行更友好的异常提示,比如:
|
||||
|
||||
```text
|
||||
// show
|
||||
{ "b"a
|
||||
^
|
||||
JSON_ERROR_001 Unexpected token "a".
|
||||
Expecting a ":" over here, eg:
|
||||
{ "b": "bar" }
|
||||
^
|
||||
You can learn more about valid JSON string in http://goo.gl/xxxxx
|
||||
```
|
||||
|
||||
更多 Demo 可以查看 [原文](https://lihautan.com/json-parser-with-javascript/)。
|
||||
|
||||
## 3 总结
|
||||
|
||||
这篇文章通过一个具体的例子解释如何做语法分析,对于词法解析入门非常直观,如果你想更深入理解语法解析,或者写一个通用语法解析器,可以阅读语法解析系列入门文章,笔者通过实际例子带你一步一步做一个完备的词法解析工具!
|
||||
|
||||
语法解析入门系列文章,建议阅读顺序:
|
||||
|
||||
- [精读《手写 SQL 编译器 - 词法分析》](https://github.com/dt-fe/weekly/blob/v2/064.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%89%8B%E5%86%99%20SQL%20%E7%BC%96%E8%AF%91%E5%99%A8%20-%20%E8%AF%8D%E6%B3%95%E5%88%86%E6%9E%90%E3%80%8B.md)
|
||||
- [精读《手写 SQL 编译器 - 文法介绍》](https://github.com/dt-fe/weekly/blob/v2/065.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%89%8B%E5%86%99%20SQL%20%E7%BC%96%E8%AF%91%E5%99%A8%20-%20%E6%96%87%E6%B3%95%E4%BB%8B%E7%BB%8D%E3%80%8B.md)
|
||||
- [精读《手写 SQL 编译器 - 语法分析》](https://github.com/dt-fe/weekly/blob/v2/066.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%89%8B%E5%86%99%20SQL%20%E7%BC%96%E8%AF%91%E5%99%A8%20-%20%E8%AF%AD%E6%B3%95%E5%88%86%E6%9E%90%E3%80%8B.md)
|
||||
- [精读《手写 SQL 编译器 - 回溯》](https://github.com/dt-fe/weekly/blob/v2/067.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%89%8B%E5%86%99%20SQL%20%E7%BC%96%E8%AF%91%E5%99%A8%20-%20%E5%9B%9E%E6%BA%AF%E3%80%8B.md)
|
||||
- [精读《手写 SQL 编译器 - 语法树》](https://github.com/dt-fe/weekly/blob/v2/070.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%89%8B%E5%86%99%20SQL%20%E7%BC%96%E8%AF%91%E5%99%A8%20-%20%E8%AF%AD%E6%B3%95%E6%A0%91%E3%80%8B.md)
|
||||
- [精读《手写 SQL 编译器 - 错误提示》](https://github.com/dt-fe/weekly/blob/v2/071.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%89%8B%E5%86%99%20SQL%20%E7%BC%96%E8%AF%91%E5%99%A8%20-%20%E9%94%99%E8%AF%AF%E6%8F%90%E7%A4%BA%E3%80%8B.md)
|
||||
- [精读《手写 SQL 编译器 - 性能优化之缓存》](https://github.com/dt-fe/weekly/blob/v2/078.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%89%8B%E5%86%99%20SQL%20%E7%BC%96%E8%AF%91%E5%99%A8%20-%20%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96%E4%B9%8B%E7%BC%93%E5%AD%98%E3%80%8B.md)
|
||||
- [精读《手写 SQL 编译器 - 智能提示》](https://github.com/dt-fe/weekly/blob/v2/085.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%89%8B%E5%86%99%20SQL%20%E7%BC%96%E8%AF%91%E5%99%A8%20-%20%E6%99%BA%E8%83%BD%E6%8F%90%E7%A4%BA%E3%80%8B.md)
|
||||
|
||||
[syntax-parser](https://github.com/ascoders/syntax-parser) 这个零依赖的通用语法解析库就是根据上述文章一步一步完成的,看完了上面文章,就彻底理解了这个库的源码。
|
||||
|
||||
> 讨论地址是:[精读《手写 JSON Parser》 · Issue #233 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/233)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
|
||||
|
||||
> 关注 **前端精读微信公众号**
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
|
||||
|
||||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
# 1 引言
|
||||
|
||||
随着前端ES6 ES7 的一路前行, 我们大前端借鉴和引进了各种其他编程语言中的概念、特性、模式;
|
||||
我们可以使用函数式Functional编程设计,可以使用面向对象OOP的设计,可以使用面向接口的思想,也可以使用AOP,
|
||||
随着前端 ES6 ES7 的一路前行, 我们大前端借鉴和引进了各种其他编程语言中的概念、特性、模式;
|
||||
我们可以使用函数式 Functional 编程设计,可以使用面向对象 OOP 的设计,可以使用面向接口的思想,也可以使用 AOP,
|
||||
可以使用注解,代理、反射,各种设计模式; 在大前端辉煌发展、在数据时代的当下 我们一起阅读了一篇设计相关的老文:
|
||||
《The DCI Architecture》
|
||||
一起来再探索和复习一下 相关的设计和思想
|
||||
@@ -11,15 +11,15 @@
|
||||
|
||||
# 2 内容摘要
|
||||
|
||||
DCI是数据Data 场景Context 交互Interactions 简称, 重点是关注 数据的不同场景的交互行为, 是面向对象系统 状态和行为的一种范式设计;
|
||||
DCI在许多方面是许多过去范式的统一,多年来这些模式已经成为面向对象编程的辅助工具。
|
||||
DCI 是数据 Data 场景 Context 交互 Interactions 简称, 重点是关注 数据的不同场景的交互行为, 是面向对象系统 状态和行为的一种范式设计;
|
||||
DCI 在许多方面是许多过去范式的统一,多年来这些模式已经成为面向对象编程的辅助工具。
|
||||
|
||||
尽管面向切面的编程(AOP)也有其他用途,但DCI满足了许多AOP的应用以及Aspects在解决问题方面的许多目标。根据AOP的基本原理,DCI基于深层次的反射或元编程。
|
||||
与Aspects不同,角色聚合并组合得很好。Context提供角色集之间的关联的范围关闭,而Aspect仅与应用它们的对象配对。
|
||||
在许多时候,虽然混合本身缺乏我们在Context语义中发现的动力 ,但DCI反映了混合风格策略。
|
||||
DCI实现了多范式设计的许多简单目标,能够将过程逻辑与对象逻辑分开。然而,DCI具有比多范式设计提供的更强大的技术更好的耦合和内聚效果
|
||||
尽管面向切面的编程(AOP)也有其他用途,但 DCI 满足了许多 AOP 的应用以及 Aspects 在解决问题方面的许多目标。根据 AOP 的基本原理,DCI 基于深层次的反射或元编程。
|
||||
与 Aspects 不同,角色聚合并组合得很好。Context 提供角色集之间的关联的范围关闭,而 Aspect 仅与应用它们的对象配对。
|
||||
在许多时候,虽然混合本身缺乏我们在 Context 语义中发现的动力 ,但 DCI 反映了混合风格策略。
|
||||
DCI 实现了多范式设计的许多简单目标,能够将过程逻辑与对象逻辑分开。然而,DCI 具有比多范式设计提供的更强大的技术更好的耦合和内聚效果
|
||||
|
||||
结合ATM 汇款场景案例,讲解了一下 DCI
|
||||
结合 ATM 汇款场景案例,讲解了一下 DCI
|
||||
角色提供了和用户相关 自然的边界,以转账为例,我们实际谈论的是钱的转移,以及源账户和目标账户的角色,算法(用例 角色行为集合)应该是这样:
|
||||
1.账户拥有人选择从一个账户到另外一个账户的钞票转移。
|
||||
2.系统显示有效账户
|
||||
@@ -35,12 +35,12 @@ DCI实现了多范式设计的许多简单目标,能够将过程逻辑与对
|
||||
2.源账户确认余额可用
|
||||
3.源账户减少其帐目
|
||||
4.源账户请求目标账户增加其帐目
|
||||
5.源账户请求目标账户更新其日志log
|
||||
5.源账户请求目标账户更新其日志 log
|
||||
6.源账户结束交易事务
|
||||
7.源账户显示给账户拥有人转账成功。
|
||||
|
||||
|
||||
```
|
||||
```plain
|
||||
template <class ConcreteAccountType>
|
||||
class TransferMoneySourceAccount: public MoneySource
|
||||
{
|
||||
@@ -88,7 +88,7 @@ DCI 尝试从人类思维角度出发,举一个例子:为什么在看电影
|
||||
2. 人或物发生了什么行为、交互?
|
||||
3. 现在在哪?厨房?太空舱?或者原始森林?
|
||||
|
||||
很快把这三件事弄清楚,我们就能快速理解当前场景的逻辑,并且**轻松理解该场景继续发生的状况**,即便是盗梦空间这种烧脑的电影,当我们搞清楚这三个问题后,就算街道发生了180度扭曲,也不会存在理解障碍,反而可以吃着爆米花享受,直到切换到下一个场景为止。
|
||||
很快把这三件事弄清楚,我们就能快速理解当前场景的逻辑,并且**轻松理解该场景继续发生的状况**,即便是盗梦空间这种烧脑的电影,当我们搞清楚这三个问题后,就算街道发生了 180 度扭曲,也不会存在理解障碍,反而可以吃着爆米花享受,直到切换到下一个场景为止。
|
||||
|
||||
当我们把街道扭曲 180 度的能力放在街道对象上时,理解就变的复杂了:这个函数什么时候被调用?为什么不好好承载车辆而自己发生扭曲?这就像电影开始时,把电影里播放的所有关于街道的状态都走马灯过一遍:我们看到街道通过了车辆、又卷曲、又发生了爆炸,实在觉得莫名其妙。
|
||||
|
||||
@@ -210,23 +210,23 @@ object MoneyTransferApp extends App {
|
||||
|
||||
- [Comparison of Architecture presentation patterns MVP(SC),MVP(PV),PM,MVVM and MVC](https://www.codeproject.com/Articles/66585/Comparison-of-Architecture-presentation-patterns-M)
|
||||
- [The DCI Architecture: A New Vision of Object-Oriented Programming](http://www.artima.com/articles/dci_vision.html)
|
||||
- [干净的架构The Clean Architecture](https://www.bbsmax.com/A/pRdBWY3ezn/)
|
||||
- [MVC的替代方案](https://gxnotes.com/article/71237.html)
|
||||
- [展示模式架构比较MVP(SC),MVP(PV),PM,MVVM和MVC](http://blog.csdn.net/lihenair/article/details/51791915)
|
||||
- [干净的架构 The Clean Architecture](https://www.bbsmax.com/A/pRdBWY3ezn/)
|
||||
- [MVC 的替代方案](https://gxnotes.com/article/71237.html)
|
||||
- [展示模式架构比较 MVP(SC),MVP(PV),PM,MVVM 和 MVC](http://blog.csdn.net/lihenair/article/details/51791915)
|
||||
- [Software Architecture Design](https://github.com/zenany/weekly/blob/master/resources/software_architecture.md)
|
||||
- [【译】什么是 Flux 架构?(兼谈 DDD 和 CQRS)](https://blog.jimmylv.info/2016-07-07-what-the-flux-on-flux-ddd-and-cqrs/)
|
||||
|
||||
|
||||
## 结合DCI 设想开发的过程中使用到一些设计方法和原则
|
||||
## 结合 DCI 设想开发的过程中使用到一些设计方法和原则
|
||||
|
||||
我们在开发的过程中多多少少都会使用到一些设计方法和原则
|
||||
DCI 重点是关注 数据的不同场景的交互行为, 是面向对象系统 状态和行为的一种范式设计;
|
||||
|
||||
它能够将过程逻辑与对象逻辑分开,是一种典型的行为模式设计;
|
||||
很好的点是 它根据AOP的基本原理,DCI 提出基于AOP 深层次的元编程(可以理解成面向接口编程), 去促使系统的内聚效果和降低耦合度;
|
||||
很好的点是 它根据 AOP 的基本原理,DCI 提出基于 AOP 深层次的元编程(可以理解成面向接口编程), 去促使系统的内聚效果和降低耦合度;
|
||||
|
||||
举个例子:
|
||||
在一个BI系统中, 在业务的发展中, 这个系统使用到了多套的 底层图表库,比如: Echarts, G2,Recharts, FusionChart; 等等;
|
||||
在一个 BI 系统中, 在业务的发展中, 这个系统使用到了多套的 底层图表库,比如: Echarts, G2,Recharts, FusionChart; 等等;
|
||||
|
||||
那么问题来了,
|
||||
1. 如何去同时支持 这些底层库, 并且达到很容易切换的一个效果?
|
||||
@@ -234,19 +234,19 @@ DCI 重点是关注 数据的不同场景的交互行为, 是面向对象系
|
||||
3. 如何去考虑扩展业务 对图表的日益增强的业务功能(如: 行列转换、智能格式化 等等)
|
||||
|
||||
带着这些问题, 我们再来看下 DCI 给我们的启示, 我们来试试看相应的解法:
|
||||
1. 图表的模型数据就是 数据Data , 我们可以把[日益增强的业务功能] 认为是各个场景交互Interactions;
|
||||
1. 图表的模型数据就是 数据 Data , 我们可以把[日益增强的业务功能] 认为是各个场景交互 Interactions;
|
||||
2. 接入更多类型的图表咋么搞?
|
||||
不同类型的图表其实是图表数据模型的转换,我们也可以把这些转换的行为过程作为一个个的切片(Aspect),每个切片都是独立的, 松耦合的 ;
|
||||

|
||||
|
||||
3. 接入多套底层库怎么搞? 每个图形库的 build方法,render 方法 , resize 方法,repaint 方法 都不一样 ,怎么搞 ? 我们可以使用 DCI 提到的元编程- 我们在这里理解为面向接口编程, 我们分装一层 统一的接口;
|
||||
3. 接入多套底层库怎么搞? 每个图形库的 build 方法,render 方法 , resize 方法,repaint 方法 都不一样 ,怎么搞 ? 我们可以使用 DCI 提到的元编程- 我们在这里理解为面向接口编程, 我们分装一层 统一的接口;
|
||||
利用面向接口的父类引用指向子类对象 我们就可以很方便的 接入更多的 implement 接入更多的图形库(当然,一个系统统一一套是最好的);
|
||||
|
||||
|
||||
|
||||
# 4 总结
|
||||
|
||||
DCI是数据Data 场景Context 交互Interactions的简称,DCI是一种特别关注行为的设计模式(行为模式),
|
||||
DCI 是数据 Data 场景 Context 交互 Interactions 的简称,DCI 是一种特别关注行为的设计模式(行为模式),
|
||||
DCI 关注数据不同场景的交互行为, 是面向对象 状态和行为的一种范式设计;DCI 尝试从人类思维,过程化设计一些行为;
|
||||
DCI 也会使用一些面向切面和接口编程的设计思想去达到高内聚低耦合的目标。
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
## 1 引言
|
||||
|
||||
拖拽是前端非常常见的交互操作,但显然拖拽是强 DOM 交互的,而 React 绕过了 DOM 这一层,那么基于 React 的拖拽方案就必定值得聊一聊。
|
||||
|
||||
结合 [How To Use The HTML Drag-And-Drop API In React](https://www.smashingmagazine.com/2020/02/html-drag-drop-api-react/) 这篇文章,让我们谈谈 React 拖拽这些事。
|
||||
|
||||
## 2 概述
|
||||
|
||||
原文说的比较简单,笔者先快速介绍其中重点部分。
|
||||
|
||||
首先拖拽主要的 API 有 4 个:`dragEnter` `dragLeave` `dragOver` `drop`,分别对应拖入、拖出、正在当前元素范围内拖拽、完成拖入动作。
|
||||
|
||||
基于这些 API,我们可以利用 React 实现一个拖入区域:
|
||||
|
||||
```jsx
|
||||
import React from "react";
|
||||
|
||||
const DragAndDrop = props => {
|
||||
const handleDragEnter = e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
};
|
||||
const handleDragLeave = e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
};
|
||||
const handleDragOver = e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
};
|
||||
const handleDrop = e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
};
|
||||
return (
|
||||
<div
|
||||
className={"drag-drop-zone"}
|
||||
onDrop={e => handleDrop(e)}
|
||||
onDragOver={e => handleDragOver(e)}
|
||||
onDragEnter={e => handleDragEnter(e)}
|
||||
onDragLeave={e => handleDragLeave(e)}
|
||||
>
|
||||
<p>Drag files here to upload</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default DragAndDrop;
|
||||
```
|
||||
|
||||
`preventDefault` 指的是阻止默认响应,这个响应可能是跳转页面之类的,`stopPropagation` 是阻止冒泡,这样同样监听了事件的父元素就不会收到响应,我们可以精准作用于嵌套的子元素。
|
||||
|
||||
接下来是拖拽状态管理,提到了 `useReducer`,顺便复习一下用法:
|
||||
|
||||
```jsx
|
||||
...
|
||||
const reducer = (state, action) => {
|
||||
switch (action.type) {
|
||||
case 'SET_DROP_DEPTH':
|
||||
return { ...state, dropDepth: action.dropDepth }
|
||||
case 'SET_IN_DROP_ZONE':
|
||||
return { ...state, inDropZone: action.inDropZone };
|
||||
case 'ADD_FILE_TO_LIST':
|
||||
return { ...state, fileList: state.fileList.concat(action.files) };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
const [data, dispatch] = React.useReducer(
|
||||
reducer, { dropDepth: 0, inDropZone: false, fileList: [] }
|
||||
)
|
||||
...
|
||||
```
|
||||
|
||||
最后一个关键点在于拖入后的处理,利用 `dispatch` 增加拖入文件、设置拖入状态即可:
|
||||
|
||||
```js
|
||||
const handleDrop = e => {
|
||||
...
|
||||
let files = [...e.dataTransfer.files];
|
||||
|
||||
if (files && files.length > 0) {
|
||||
const existingFiles = data.fileList.map(f => f.name)
|
||||
files = files.filter(f => !existingFiles.includes(f.name))
|
||||
|
||||
dispatch({ type: 'ADD_FILE_TO_LIST', files });
|
||||
e.dataTransfer.clearData();
|
||||
dispatch({ type: 'SET_DROP_DEPTH', dropDepth: 0 });
|
||||
dispatch({ type: 'SET_IN_DROP_ZONE', inDropZone: false });
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
`e.dataTransfer.clearData` 函数用于清除拖拽过程中产生的临时变量,这些临时变量可以通过 `e.dataTransfer.xxx =` 的方式赋值,一般用于拖拽过程中值的传递。
|
||||
|
||||
总结一下,利用 HTML5 的 API 将拖拽转化为状态,最终通过状态映射到 UI。
|
||||
|
||||
原文内容还是比较简单的,笔者在精读部分再拓展一些更体系化的内容。
|
||||
|
||||
## 3 精读
|
||||
|
||||
现阶段拖拽主要分为两种,一种是 HTML5 原生规范的拖拽,这种方式在拖拽过程中不会影响 DOM 结构。另一种是完全所见即所得的拖拽方式,拖拽过程中 DOM 位置会随之变动,好处是可以立即反馈拖拽结果,当然缺点是华而不实,一旦用在生产环境,这种拖拽过程可能导致页面结构频繁跳动,反而看不清拖拽效果。
|
||||
|
||||
由于本文也采用了第一种拖拽方案,因为笔者再重新整理一遍自己的封装思路。
|
||||
|
||||
从使用角度反推,假设我们拥有一个拖拽库,那必定要拥有两个 API:
|
||||
|
||||
```jsx
|
||||
import { DragContainer, DropContainer } from 'dnd'
|
||||
|
||||
const DragItem = (
|
||||
<DragContainer>
|
||||
{({ dragProps }) => (
|
||||
<div {...dragProps} />
|
||||
)}
|
||||
</DragContainer>
|
||||
)
|
||||
|
||||
const DropItem = (
|
||||
<DropContainer>
|
||||
{({ dropProps }) => (
|
||||
<div {...dropProps} />
|
||||
)}
|
||||
</DropContainer>
|
||||
)
|
||||
```
|
||||
|
||||
`DragContainer` 包裹可以被拖拽的元素,`DropContainer` 包裹可以被拖入的元素,而至于 `dragProps` 与 `dropProps` 需要透传到子元素的 dom 节点,是为了利用 DOM API 控制拖拽效果,这也是拖拽唯一对 DOM 的要求,双方元素都需要有实体 DOM 承载。
|
||||
|
||||
而上面例子中给出 `dragProps` 与 `dropProps` 的方式属于 RenderProps,我们可以将 `children` 当作函数执行以达到效果:
|
||||
|
||||
```jsx
|
||||
const DragContainer = ({ children, componentId }) => {
|
||||
const { dragProps } = useDnd(componentId)
|
||||
|
||||
return children({
|
||||
dragProps
|
||||
})
|
||||
}
|
||||
|
||||
const DropContainer = ({ children, componentId }) => {
|
||||
const { dropProps } = useDnd(componentId)
|
||||
|
||||
return children({
|
||||
dropProps
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
那么这里创建了一个自定义 Hook `useDnd` 接收 `dragProps` 与 `dropProps`,这个自定义 Hook 可以这么写:
|
||||
|
||||
```jsx
|
||||
const useDnd = ({ componentId }) => {
|
||||
const dragProps = {}
|
||||
const dropProps = {}
|
||||
|
||||
return { dragProps, dropProps }
|
||||
}
|
||||
```
|
||||
|
||||
接下来,我们就要分别实现 `drag` 与 `drop` 了。
|
||||
|
||||
对 `drag` 来说,只要实现 `onDragStart` 与 `onDragEnd` 即可:
|
||||
|
||||
```jsx
|
||||
const dragProps = {
|
||||
onDragStart: ev => {
|
||||
ev.stopPropagation()
|
||||
ev.dataTransfer.setData('componentId', componentId)
|
||||
},
|
||||
onDragEnd: ev => {
|
||||
// 做一些拖拽结束的清理工作
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`stopPropagation` 的作用在原文简介中已经介绍过了,`setData` 则是通知拖拽方,当前拖拽的组件 id 是什么,**这是由于拖拽由 `drag` 发起而由 `drop` 响应,因此必须有个数据传输过程,而 `dataTransfer` 就最适合做这件事。**
|
||||
|
||||
对于 `drop` 来说,只要实现 `onDragOver` 与 `onDrop` 即可:
|
||||
|
||||
```jsx
|
||||
const dropProps = {
|
||||
onDragOver: ev => {
|
||||
// 做一些样式处理,提示用户此时松手会将元素放置在何处
|
||||
},
|
||||
onDrop: ev => {
|
||||
ev.stopPropagation()
|
||||
const componentId = ev.dataTransfer.getData('componentId')
|
||||
// 通过 componentId 修改数据,通过 React Rerender 刷新 UI
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
重点在 `onDrop`,它是实现拖拽效果的 “真正执行处”,最终通过修改 UI 的方式更新数据。
|
||||
|
||||
存在一种场景,一个容器既可以被拖动,也可以被拖入,这种情况一般这个组件是个容器,但这个容器可以被拖入到其他容器中,可以自由嵌套。
|
||||
|
||||
实现这种场景的方式就是将 `DragContainer` 与 `DropContainer` 作用到一个组件上:
|
||||
|
||||
```jsx
|
||||
const Box = (
|
||||
<DragContainer>
|
||||
{({ dragProps }) => (
|
||||
<DropContainer>
|
||||
{({ dropProps }) => {
|
||||
<div {...dragProps} {...dropProps} />
|
||||
}}
|
||||
</DropContainer>
|
||||
)}
|
||||
</DragContainer>
|
||||
)
|
||||
```
|
||||
|
||||
之所以能嵌套,在于 HTML5 的 API 允许一个元素同时拥有 `onDragStart`、`onDrop` 这两种属性,而上面的语法不过是同时将这两种属性传给组件 DOM。
|
||||
|
||||
所以,动手实现一个拖拽库就是这么简单,只要活用 HTML5 的拖拽 API,结合 React 一些特殊语法便够了。
|
||||
|
||||
## 4 总结
|
||||
|
||||
最后留下一个思考题,许多具有拖拽功能的系统都具备 “拖拽 placeholder” 的功能,即拖拽元素的过程中,在其 “落点” 位置展示一条横线或竖线,引导出松手后元素位置落点,如图所示:
|
||||
|
||||
<img width=400 src="https://img.alicdn.com/tfs/TB11H04wbY1gK0jSZTEXXXDQVXa-1434-384.png">
|
||||
|
||||
那么这条辅助线是通过什么方式实现的呢?欢迎在评论区留言!如果你有辅助线实现方案解析的文章,欢迎分享,也可以期待笔者未来专门写一篇 “拖拽 placeholder” 实现剖析的精读。
|
||||
|
||||
> 讨论地址是:[精读《手写 JSON Parser》 · Issue #233 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/233)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
|
||||
|
||||
> 关注 **前端精读微信公众号**
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
|
||||
|
||||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
|
||||
@@ -0,0 +1,108 @@
|
||||
## 1 引言
|
||||
|
||||
`useRef` 是常用的 API,但还有一个 `createRef` 的 API,你知道他们的区别吗?通过 [React.useRef and React.createRef: The Difference](https://blog.bitsrc.io/react-useref-and-react-createref-the-difference-afedb9877d0f) 这篇文章,你可以了解到何时该使用它们。
|
||||
|
||||
## 2 概述
|
||||
|
||||
其实原文就阐述了这样一个事实:`useRef` 仅能用在 FunctionComponent,`createRef` 仅能用在 ClassComponent。
|
||||
|
||||
第一句话是显然的,因为 Hooks 不能用在 ClassComponent。
|
||||
|
||||
第二句话的原因是,`createRef` 并没有 Hooks 的效果,其值会随着 FunctionComponent 重复执行而不断被初始化:
|
||||
|
||||
```tsx
|
||||
function App() {
|
||||
// 错误用法,永远也拿不到 ref
|
||||
const valueRef = React.createRef();
|
||||
return <div ref={valueRef} />;
|
||||
}
|
||||
```
|
||||
|
||||
上述 `valueRef` 会随着 App 函数的 Render 而重复初始化,**这也是 Hooks 的独特之处,虽然用在普通函数中,但在 React 引擎中会得到超出普通函数的表现,比如初始化仅执行一次,或者引用不变**。
|
||||
|
||||
为什么 `createRef` 可以在 ClassComponent 正常运行呢?这是因为 ClassComponent 分离了生命周期,使例如 `componentDidMount` 等初始化时机仅执行一次。
|
||||
|
||||
原文完。
|
||||
|
||||
## 3 精读
|
||||
|
||||
那么知道如何正确创建 Ref 后,还知道如何正确更新 Ref 吗?
|
||||
|
||||
由于 Ref 是贯穿 FunctionComponent 所有渲染周期的实例,理论上在任何地方都可以做修改,比如:
|
||||
|
||||
```tsx
|
||||
function App() {
|
||||
const valueRef = React.useRef();
|
||||
|
||||
valueRef.current += 1;
|
||||
|
||||
return <div />;
|
||||
}
|
||||
```
|
||||
|
||||
但其实上面的修改方式是不规范的,React 官方文档里要求我们避免在 Render 函数中直接修改 Ref,请先看下面的 FunctionComponent 生命周期图:
|
||||
|
||||
<img width=600 src="https://img.alicdn.com/tfs/TB12aHDwQL0gK0jSZFtXXXQCXXa-3300-2550.png">
|
||||
|
||||
从图中可以发现,在 `Render phase` 阶段是不允许做 “side effects” 的,也就是写副作用代码,这是因为这个阶段可能会被 React 引擎随时取消或重做。
|
||||
|
||||
修改 Ref 属于副作用操作,因此不适合在这个阶段进行。我们可以看到,在 `Commit phase` 阶段可以做这件事,或者在回调函数中做(脱离了 React 生命周期)。
|
||||
|
||||
当然有一种情况是可以的,即 [懒初始化](https://reactjs.org/docs/hooks-faq.html#how-to-create-expensive-objects-lazily):
|
||||
|
||||
```ts
|
||||
function Image(props) {
|
||||
const ref = useRef(null);
|
||||
|
||||
// ✅ IntersectionObserver is created lazily once
|
||||
function getObserver() {
|
||||
if (ref.current === null) {
|
||||
ref.current = new IntersectionObserver(onIntersect);
|
||||
}
|
||||
return ref.current;
|
||||
}
|
||||
|
||||
// When you need it, call getObserver()
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
懒初始化的情况下,副作用最多执行一次,而且仅用于初始化赋值,所以这种行为是被允许的。
|
||||
|
||||
为什么对副作用限制的如此严格?因为 FunctionComponent 增加了内置调度系统,为了优先响应用户操作,可能会暂定某个 React 组件的渲染,具体可以看第 99 篇精读:[精读《Scheduling in React》](https://github.com/dt-fe/weekly/blob/v2/099.%E7%B2%BE%E8%AF%BB%E3%80%8AScheduling%20in%20React%E3%80%8B.md)
|
||||
|
||||
Ref 不仅可以拿到组件引用、创建一个 Mutable 副作用对象,还可以配合 `useEffect` 存储一个较老的值,最常用来拿到 `previousProps`,React 官方利用 Ref 封装了一个简单的 Hooks 拿到上一次的值:
|
||||
|
||||
```tsx
|
||||
function usePrevious(value) {
|
||||
const ref = useRef();
|
||||
useEffect(() => {
|
||||
ref.current = value;
|
||||
});
|
||||
return ref.current;
|
||||
}
|
||||
```
|
||||
|
||||
由于 `useEffect` 在 Render 完毕后才执行,因此 `ref` 的值在当前 Render 中永远是上一次 Render 时候的,我们可以利用它拿到上一次 Props:
|
||||
|
||||
```tsx
|
||||
function App(props) {
|
||||
const preProps = usePrevious(props);
|
||||
}
|
||||
```
|
||||
|
||||
要实现这个功能,还是要归功于 `ref` 可以将值 “在各个不同的 Render 闭包中传递的特性”。最后,不要滥用 Ref,Mutable 引用越多,对 React 来说可维护性一般会越差。
|
||||
|
||||
## 4 总结
|
||||
|
||||
你还挖掘了 `useRef` 哪些有意思的使用方式?欢迎在评论区留言。
|
||||
|
||||
> 讨论地址是:[精读《useRef 与 createRef 的区别》 · Issue #236 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/236)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
|
||||
|
||||
> 关注 **前端精读微信公众号**
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
|
||||
|
||||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
|
||||
@@ -0,0 +1,97 @@
|
||||
## 1 引言
|
||||
|
||||
任何软件都是协同开发的,所以 CodeReview 非常重要,它可以帮助你减少代码质量问题,提高开发效率,提升稳定性,同时还能保证软件架构的稳定性,防止代码结构被恶意破坏导致难以维护。
|
||||
|
||||
所以 CodeReview 机制是否健全是一个工程团队能否长期健康发展的决定因素之一,这次我们读一篇关于 CodeReview 如何做得更好的文章: [how-to-make-good-code-reviews-better](https://stackoverflow.blog/2019/09/30/how-to-make-good-code-reviews-better/)。
|
||||
|
||||
## 2 概述 & 精读
|
||||
|
||||
作者结合自己在 Uber、微软的工作经历介绍了自己对如何做好 CodeReview 的看法。
|
||||
|
||||
### CodeReview 的覆盖范围
|
||||
|
||||
**Good CodeReview** 会检查代码的正确性、测试覆盖率、功能变化、是否遵循代码规范与最佳实践、可以指出一些较为明显的改进点,比如难以阅读的写法、未使用到变量、一些边界问题、commit 数量过大需要拆分等等。
|
||||
|
||||
**Better CodeReview** 会检查引入代码的必要性,与已有系统是否适配,是否具有可维护性,从抽象角度思考代码是否与已有系统逻辑能够自洽。
|
||||
|
||||
> Better CodeReview 会关注在可维护性层面,并具有全局性,往往几个局部正确的代码组合在一起会产生错误的结果,或者是没必要的代码,或者是相互冲突的逻辑。Better CodeReview 更多用在底层架构场景,因为架构底层模块关联比较紧密,需要有整体视角,而业务上层模块间最好采用解耦模式,这样不仅不需要更耗费精力的 Better CodeReview,也是一种更正确的架构设计。
|
||||
|
||||
### CodeReview 的语气
|
||||
|
||||
**Good CodeReview** 会给出建设性意见,而不是发表强硬措辞要求对方改正,或认为自己的意见是唯一正确的答案,因为这样的评论其实具有一定攻击性,激发对方的防御心理,产生敌对心态,这样会从内部瓦解一个团队。最好能给出建议,或者多个选择,给对方留有余地。
|
||||
|
||||
**Better CodeReview** 永远是考虑全面且正向积极的,会对写的好的地方进行鼓励,对写的不好的地方也体现出善解人意的关怀,考虑到对方可能花费了很多心血,以一种换位思考的鼓励心态进行评论。
|
||||
|
||||
> 其实读到语气这一章节,逐渐发现 CodeReview 不仅是一个技术专业行为,还是一个人与人相处的社交行为,有的人平时与人打交道非常谦逊,但在 CodeReview 中就变得尖酸刻薄,显然是只关注到了 CodeReview 的专业性这一面,忽略了社交性这一点。而要做到 Better CodeReview 还要学会换位思考,体现出包容、正向积极的态度,因为你技术经验更丰富,能指出别人的问题很正常,但能保持谦逊,让别人容易接受并受到鼓励,可以让你成为一个有气度的技术专家。
|
||||
|
||||
### 如何完成 CodeReview 的审阅
|
||||
|
||||
**Good CodeReview** 不会轻易通过那些开放式 PR,至少在其被得到充分讨论前,但每个 Review 者对自己关注的部分完成 Review 后需要进行反馈,无论是 “看起来不错” 或者用缩写单词 “LGTM”,之后需要有明确的跟进,比如通过协作软件通知作者进行进一步反馈。
|
||||
|
||||
**Better CodeReview** 实际执行中会更加灵活一些,对于一些比较紧急的改动会留下改进建议,但快速通过,让作者通过后续代码提交解决遗留的问题。
|
||||
|
||||
> 实际工作场景会遇到一些开放式或紧急的提交,良好的 CodeReview 习惯自然是要严谨一些,讨论清楚再通过,并且要及时反馈。但某些比较紧急的提交就要区别对待了,更好的态度是在实践中灵活对待,但及时紧急通过了,也要保证问题在后续得以修复,比如在代码中留一些 "TODO" 或 "FIXME" 的标记,写上对应的负责人与预期解决时间。
|
||||
|
||||
### 从 CodeReview 到直接交流
|
||||
|
||||
**Good CodeReview** 会给出完整的评论和修改建议,如果后续提交的代码不符合预期,Review 者可以直接与代码提交者面对面交流,这样可以避免后续花费更多沟通时间。
|
||||
|
||||
**Better CodeReview** 会在第一次给出完整的评论和修改建议,如果后续提交代码不符合预期,会立即与代码提交者当面沟通,避免异步沟通带来更多的理解偏差。
|
||||
|
||||
> 补充一下,在 PR 内容过多时也可以选择直接与提交者当面沟通,这样可以更多理解作者的想法,使 Review 准确性更高。另外并不要每次都直接交流,异步的 CodeReview 本身就是一种提效方案,这会使你工作节奏把握在自己手中,仅在这种方案出现沟通问题时再选择当面交流。
|
||||
|
||||
### 区分重点
|
||||
|
||||
**Good CodeReview** 可以区分提示的重要程度,并在不太重要的改动前面加上 “nit:” 标记,这样可以使提交者的注意力集中在重要的问题上。
|
||||
|
||||
**Better CodeReview** 会采取工具手段解决这些问题,比如一些代码 lint 工具,因为这些问题往往是可以被工具自动化解决的。
|
||||
|
||||
> 代码自动化工具的目的,很大一部分也是为了保证代码一致性,从而降低 CodeReview 成本,也减少不重要的评论信息出现,让 CodeReview 尽可能反馈逻辑问题而不是格式问题。
|
||||
|
||||
### 针对新人的 CodeReview
|
||||
|
||||
**Good CodeReview** 对任何人都是用相同评判标准,可以遵循上面几点注意事项。
|
||||
|
||||
**Better CodeReview** 会对新人区分对待,对新人给予对多的耐心、解释和评论,甚至给出解决方法,并更积极的给出鼓励。
|
||||
|
||||
> 任何人到一家新公司都有适应过程,一视同仁是 base 要求,但如果能给予新人更多关怀就更好啦。
|
||||
|
||||
### 跨办公区、时区的 CodeReview
|
||||
|
||||
**Good CodeReview** 仅在工作时间有重叠的时间范围内进行 CodeReview,这样能保证对方可以积极响应,在必要时进行语音、视频沟通。
|
||||
|
||||
**Better CodeReview** 会注意到更本质的问题,留意跨团队协作的必要性,如果某个模块经常被另一个时区同时修改,也许可以将这个模块交给对方维护,或者将 CodeReview 交给对方团队内部进行会更加高效。
|
||||
|
||||
> 笔者所在公司也有跨时区协作情况,但绝大部分场景会避免跨时区的 CodeReview,因为 CodeReview 一般会在同一时区团队内部进行,这样效率更高,应对跨时区协作时,往往也是电话、视频会议优先。
|
||||
|
||||
### 公司支持
|
||||
|
||||
**Good CodeReview** 会得到公司组织支持,公司能意识到这么做虽然看起来占用开发时间,但长远来看提升了开发效率,因此能任何 CodeReview 价值。
|
||||
|
||||
**Better CodeReview** 会得到公司进一步支持,公司甚至不断研发并完善 CodeReview 系统与流程,通过系统化方案保证上面几项 CodeReview 注意事项是否有在团队内落实,可以全员参与。
|
||||
|
||||
> CodeReview 也是一种团队文化和公司文化,公司文化带来的是规章制度与系统工具,团队文化带来的是良好 CodeReview 氛围与更高 CodeReview 的效率。
|
||||
|
||||
## 3 总结
|
||||
|
||||
总结一下,良好的 CodeReview 需要做到以下几点:
|
||||
|
||||
1. 更全面,从正确性到系统影响评估。
|
||||
2. 注意语气,从给出建设性一觉到换位思考。
|
||||
3. 及时完成审阅,从充分讨论到随机应变。
|
||||
4. 加强交流,从面对面交流到灵活选择最高效的沟通方式。
|
||||
5. 区分重点,从添加标记到利用工程化工具自动解决。
|
||||
6. 对新人要更友好。
|
||||
7. 尽量避免跨时区协作,必要时选择视频会议。
|
||||
|
||||
最后,希望 CodeReview 能够得到公司的支持,如果你们公司还没有认可 CodeReview 的价值,可以将这篇文章分享给你的领导。
|
||||
|
||||
> 讨论地址是:[精读《如何做好 CodeReview》 · Issue #237 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/237)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
|
||||
|
||||
> 关注 **前端精读微信公众号**
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
|
||||
|
||||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
|
||||